Viết code dễ đổi, dễ test như thế nào?

Bài viết được sự cho phép của tác giả Nguyễn Việt Hưng

Các lập trình viên chuyển sang code Python từ các ngôn ngữ lập trình khác như Java, C, Golang… thường bắt đầu code bằng việc bật một cái IDE to đùng (PyCharm) lên, rồi viết chục dòng code, sau đó bấm nút “tam giác” để chạy từ trên xuống dưới. Đó là cách làm phổ biến, tiêu chuẩn khi viết code C, Java, Golang… nhưng là một cách làm rất không … Python.

Khi học Python, việc đầu tiên ta làm là bật python từ terminal, rồi gõ trực tiếp các dòng code vào đó, enter để thấy kết quả:

$

python3

Python

3.6

.

9

(

default

,

Apr

18

2020

,

01

:

56

:

04

)

[

GCC

8.4

.

0

]

on

linux

Type

"help"

,

"copyright"

,

"credits"

or

"license"

for

more

information

.

'42 is the answer of life.'

Còn khi đi làm, viết code Python? Cũng vậy!

Khả năng gõ code trực tiếp, enter thấy ngay kết quả như trên, là một tính năng cực kỳ hấp dẫn/quan trọng của Python cũng như các ngôn ngữ lập trình có REPL như Ruby, Clojure, JavaScript, LISP, Ocaml, Elixir, F#… nó cho phép người dùng khám phá, vui chơi thoải mái với dữ liệu một cách tương tác, thấy kết quả nhanh nhất, thay vì phải ngồi tưởng tượng, đoán, chờ compile, và dựa vào IDE trợ giúp như các ngôn ngữ không có REPL.

Đây là chế độ “interactive mode” của Python interpreter, khái niệm này có cái tên khác chung hơn là: REPL.

(Chú ý: Golang có các project như gore hay yaegi nhưng đều rất hạn chế so với REPL của các ngôn ngữ kể trên).

REPL

REPL – Read Eval Print Loop, là môi trường nhận đầu vào từ người dùng (Read), chạy input đó (Eval), in kết quả ra màn hình (Print), và cứ tiếp tục vậy (Loop).

Khái niệm này bắt nguồn từ ngôn ngữ lập trình cổ thứ 2 thế giới: LISP.

Việc viết code khi dùng các ngôn ngữ có REPL thường theo các bước:

  • bật REPL lên
  • gõ code thử cho tới khi thu được kết quả mong muốn
  • copy code đó vào editor/IDE

Ví dụ

# githubstars.py

from

urllib.request

import

urlopen

import

json

def

main

():

repos

=

json

.

load

(

f

)

has_stars

=

[]

for

repo

in

repos

:

has_stars

.

append

((

repo

[

"stargazers_count"

],

repo

[

"html_url"

]

))

has_stars

.

sort

(

reverse

=

True

)

for

stars

,

url

in

has_stars

:

output

=

"{} - {}"

.

format

(

stars

,

url

)

print

(

output

)

if

__name__

==

"__main__"

:

main

()

Nếu viết theo kiểu này, rồi cho vào IDE, bấm nút tam giác để chạy, những nhược điểm sau sẽ xuất hiện:

  • Mỗi lần chạy, code sẽ truy cập vào API GitHub 1 lần, việc này ngoài chậm, phụ thuộc vào mạng internet mỗi lần chạy, còn thêm nhược điểm nữa là sẽ dùng tốn “quota” hàng ngày của bạn (VD GitHub chỉ cho phép gọi API n lần 1 ngày).
  • Trừ khi bạn code 1 lần chuẩn luôn, còn không thì mất khoảng 5 7 lần mới ra đoạn code trên.
  • Không test từng phần (bước) của đoạn code được.

Thay vì vậy, viết lại một phần code như sau

from

urllib.request

import

urlopen

import

json

def

getrepos

():

repos

=

json

.

load

(

f

)

return

repos

def

main

():

pass

Lưu vào file github.py, rồi vào terminal, bật python3 lên, gõ:

0

8

print

(

fmt

.

format

(

*

i

))

Với cách làm này, chỉ cần gọi GitHub API duy nhất 1 lần, còn sau đó thử thoải mái cho đến khi thu được kết quả mong muốn thì copy vào file cuối cùng:

import

json

from

urllib.request

import

urlopen

def

getrepos

():

repos

=

json

.

load

(

f

)

return

repos

def

has_stars

(

repo

):

def

filter_repos_have_stars

(

repos

):

return

[

p

for

p

in

repos

if

has_stars

(

p

)]

def

get_star_url

(

p

):

return

(

p

[

"stargazers_count"

],

p

[

"html_url"

])

def

main

():

repos

=

getrepos

()

repos_have_stars

=

filter_repos_have_stars

(

repos

)

stars_urls

=

[

get_star_url

(

p

)

for

p

in

repos_have_stars

]

stars_urls

.

sort

(

reverse

=

True

)

fmt

=

"{} - {}"

for

i

in

stars_urls

:

print

(

fmt

.

format

(

*

i

))

if

__name__

==

"__main__"

:

main

()

Sau này nếu code có bug, lại bật REPL lên, gọi các function để debug trực tiếp dễ dàng, từng bước một.

$

ipython

Python

3.6

.

9

(

default

,

Jul

17

2020

,

12

:

50

:

27

)

Type

'copyright'

,

'credits'

or

'license'

for

more

information

IPython

7.9

.

0

--

An

enhanced

Interactive

Python

.

Type

'?'

for

help

.

In

[

1

]:

import

github

In

[

2

]:

repos

=

github

.

getrepos

()

In

[

3

]:

have_stars

=

github

.

filter_repos_have_stars

(

repos

)

In

[

4

]:

len

(

have_stars

)

Out

[

4

]:

8

In

[

5

]:

[

github

.

get_star_url

(

p

)

for

p

in

have_stars

]

Out

[

5

]:

In

[

8

]:

sorted

([

github

.

get_star_url

(

p

)

for

p

in

have_stars

],

reverse

=

True

)

Out

[

8

]:

Dev với IPython

IPython (pip install ipython) cung cấp thêm các tính năng giúp cách code này hiệu quả hơn.

IPython có màu mè, auto-indent tự thụt sau for/if giúp gõ nhanh hơn.

Magic command %hist sẽ hiện full history những gì user đã gõ, giúp copy code để paste ra IDE/Editor dễ hơn, không bao gồm output.

Magic command %edit sẽ mở hẳn editor ra để sửa code, sau khi đóng lại, code sẽ được chạy, các biến sẽ tồn tại trong môi trường đang code.

Ví dụ này gõ %edit lần đầu định nghĩa list ns, rồi gõ %edit lần 2 để print ra list ns định nghĩa trước đó:

$

ipython

Python

3.6

.

9

(

default

,

Apr

18

2020

,

01

:

56

:

04

)

Type

'copyright'

,

'credits'

or

'license'

for

more

information

IPython

7.9

.

0

--

An

enhanced

Interactive

Python

.

Type

'?'

for

help

.

In

[

1

]:

%

edit

IPython

will

make

a

temporary

file

named

:

/

tmp

/

ipython_edit_2v90rimj

/

ipython_edit_wf_rn_nc

.

py

Editing

...

done

.

Executing

edited

code

...

Out

[

1

]:

'

n

ns = [1,2,3,4]

n

'

In

[

2

]:

ns

Out

[

2

]:

[

1

,

2

,

3

,

4

]

In

[

3

]:

%

edit

IPython

will

make

a

temporary

file

named

:

/

tmp

/

ipython_edit_qb43pml6

/

ipython_edit_tph7_5x6

.

py

Editing

...

done

.

Executing

edited

code

...

[

1

,

2

,

3

,

4

]

Out

[

3

]:

'print(ns)

n

'

Đổi editor

ra shell, gõ echo $EDITOR xem đang đặt là gì, thay bằng câu lệnh mở editor mình muốn, ví dụ

$ 

export

EDITOR

=

nano $ ipython

Chạy file rồi bật REPL

Python hay IPython đều hỗ trợ option -i, sau khi chạy với 1 file code sẽ tự động vào chế độ interactive mode

Jupyter

Unittest

Trong các ngôn ngữ không có REPL, cách thử 1 đoạn code nhanh nhất là viết 1 function cần thử, rồi viết unittest, rồi chạy test thay vì chạy cả 1 chương trình ngàn dòng. Với Python, ta chỉ cần bật REPL lên, import module vào và khám phá.

Code viết theo cách mới trên vừa dễ gõ trực tiếp trong REPL, vừa dễ viết unittest, ví dụ viết nhanh unittest chạy bằng pytest (pip install pytest) như sau:

# test_github.py

import

github

def

test

():

bad

=

{

"stargazers_count"

:

0

,

"html_url"

:

"bad_repo"

}

good

=

{

"stargazers_count"

:

69

,

"blah"

:

"blo"

,

}

sample_repos

=

[

bad

,

good

]

assert

github

.

filter_repos_have_stars

(

sample_repos

)

==

[

good

]

assert

github

.

get_star_url

(

good

)

==

(

good

[

"stargazers_count"

],

good

[

"html_url"

],

)

assert

github

.

has_stars

(

bad

)

is

False

assert

github

.

has_stars

(

good

)

is

True

Viết code bằng REPL hay bằng unittest TDD đều mang tới một kết quả chung: code dễ sửa, dễ test.

Tất nhiên REPL không thay thế hoàn toàn cho unittest, nhưng nó mang lại môi trường thử nghiệm nhanh chóng tương đương như hơn nhiều unittest ở các ngôn ngữ khác.

Hành động của chúng ta

Cài ngay IPython, Jupyter rồi bật lên mỗi khi muốn code Python.

Kết luận

Đừng đọc tiếng Anh theo kiểu Tiếng Việt, đừng code Python theo kiểu Java. REPL là một phát minh có sức mạnh khủng khiếp mà các Pythonista nên vận dụng, sử dụng, và lạm dụng hết mình.