1. Code
  2. Python

Cách Viết, Đóng gói và Phân phối một Thư viện trong Python

Python là một ngôn ngữ lập trình tuyệt vời, nhưng việc đóng gói là một trong những điểm yếu nhất của nó. Nó là một thực tế được nhiều người biết đến trong cộng đồng. Việc cài đặt, import, sử dụng và tạo các gói đã được cải thiện rất nhiều trong nhiều năm qua, nhưng nó vẫn chưa bằng với những ngôn ngữ mới như Go và Rust những ngôn ngữ đã học hỏi rất nhiều từ những bất cập của Python và các ngôn ngữ lâu đời khác.
Scroll to top

Vietnamese (Tiếng Việt) translation by Dai Phong (you can also view the original English article)

Python là một ngôn ngữ lập trình tuyệt vời, nhưng việc đóng gói là một trong những điểm yếu nhất của nó. Nó là một thực tế được nhiều người biết đến trong cộng đồng. Việc cài đặt, import, sử dụng và tạo các gói đã được cải thiện rất nhiều trong nhiều năm qua, nhưng nó vẫn chưa bằng với những ngôn ngữ mới như Go và Rust những ngôn ngữ đã học hỏi rất nhiều từ những bất cập của Python và các ngôn ngữ lâu đời khác.

Trong bài hướng dẫn này, bạn sẽ học mọi thứ cần biết liên quan đến viết, đóng gói và phân phối các gói của riêng bạn.

Cách viết một Thư viện Python

Thư viện Python là bộ liên kết các mô đun Python được tổ chức dưới dạng một gói Python. Nói chung, điều đó có nghĩa là tất cả các mô-đun nằm trong cùng một thư mục và rằng thư mục này nằm trên đường dẫn tìm kiếm Python.

Hãy viết nhanh một gói Python 3 nho nhỏ và làm rõ tất cả những khái niệm này.

Pathology Package

Python 3 có một đối tượng Path tuyệt vời, là một cải tiến lớn so với mô-đun os.path vụng về của Python 2. Nhưng nó thiếu một khả năng quan trọng - tìm kiếm đường dẫn của script hiện tại. Điều này là rất quan trọng khi bạn muốn xác định vị trí của các tập tin truy cập liên quan đến script hiện tại.

Trong nhiều trường hợp, script có thể được cài đặt ở bất kỳ vị trí nào, vì vậy bạn không thể sử dụng các đường dẫn tuyệt đối, và thư mục hiện hành có thể được thiết lập thành bất kỳ giá trị nào, vì vậy bạn không thể sử dụng một đường dẫn tương đối. Nếu bạn muốn truy cập vào một tập tin trong một thư mục con hoặc thư mục cha, bạn phải có khả năng tìm ra thư mục của script hiện tại.

Đây là cách bạn làm điều đó trong Python:

1
import pathlib
2
3
script_dir = pathlib.Path(__file__).parent.resolve()

Để truy cập vào một tập tin có tên là 'file.txt' trong một thư mục con 'data' trong thư mục hiện tại của script, bạn có thể sử dụng code sau: print(open(str(script_dir/'data/file.txt').read())

Với pathology package, bạn có một phương thức script_dir được tích hợp sẵn, và bạn sử dụng nó như sau:

1
from pathology.Path import script_dir
2
3
print(open(str(script_dir()/'data/file.txt').read())
4

Vâng, nó có chút xíu thôi. Pathology package rất đơn giản. Nó dẫn xuất lớp Path của riêng nó từ Path của pathlib và thêm một hàm tĩnh script_dir() luôn trả về đường dẫn của script đang gọi.

Dưới đây là phần cài đặt:

1
import pathlib
2
import inspect
3
4
class Path(type(pathlib.Path())):
5
    @staticmethod
6
    def script_dir():
7
        print(inspect.stack()[1].filename)
8
        p = pathlib.Path(inspect.stack()[1].filename)
9
        return p.parent.resolve()

Do việc cài đặt đa nền tảng của pathlib.Path, bạn có thể dẫn xuất trực tiếp từ nó và phải dẫn xuất từ một lớp con cụ thể (PosixPath hoặc WindowsPath). Sự phân giải thư mục script sử dụng mô-đun inspect để tìm script gọi và sau đó thuộc tính tên tập tin của nó.

Thử nghiệm Pathology Package

Bất cứ khi nào bạn viết ra một cái gì đó quan trọng, bạn nên kiểm thử nó. Mô-đun pathology cũng không ngoại lệ. Dưới đây là các bài kiểm tra sử dụng framework unit test tiêu chuẩn:

1
import os
2
import shutil 
3
from unittest import TestCase
4
from pathology.path import Path
5
6
7
class PathTest(TestCase):
8
    def test_script_dir(self):
9
        expected = os.path.abspath(os.path.dirname(__file__))
10
        actual = str(Path.script_dir())
11
        self.assertEqual(expected, actual)
12
13
    def test_file_access(self):
14
        script_dir = os.path.abspath(os.path.dirname(__file__))
15
        subdir = os.path.join(script_dir, 'test_data')
16
        if Path(subdir).is_dir():
17
            shutil.rmtree(subdir)
18
        os.makedirs(subdir)
19
        file_path = str(Path(subdir)/'file.txt')
20
        content = '123'
21
        open(file_path, 'w').write(content)
22
        test_path = Path.script_dir()/subdir/'file.txt'
23
        actual = open(str(test_path)).read()
24
25
        self.assertEqual(content, actual)

Đường dẫn Python

Các gói Python phải được cài đặt ở đâu đó trên đường dẫn tìm kiếm của Python để được import bởi các mô-đun của Python. Đường dẫn tìm kiếm của Python là một danh sách các thư mục và luôn có sẵn trong sys.path. Dưới đây là sys.path hiện tại của tôi:

1
>>> print('\n'.join(sys.path))
2
3
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python36.zip
4
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6
5
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/lib-dynload
6
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/site-packages
7
/Users/gigi.sayfan/miniconda3/envs/py3/lib/python3.6/site-packages/setuptools-27.2.0-py3.6.egg 

Lưu ý dòng trống đầu tiên của đầu ra đại diện cho thư mục hiện tại, vì vậy bạn có thể import các mô-đun từ thư mục hiện hành, bất kể nó là gì. Bạn có thể trực tiếp thêm hoặc xóa bớt các thư mục trong sys.path.

Bạn cũng có thể định nghĩa một biến môi trường PYTHONPATH, và có một vài cách khác để kiểm soát nó. site-packages tiêu chuẩn được bao gồm mặc định, và đây là nơi các gói mà bạn cài đặt bằng cách sử dụng pip go.

Cách Đóng gói một Thư viện Python

Bây giờ chúng ta có code và các bài kiểm tra, hãy đóng gói tất cả nó thành một thư viện thích hợp. Python cung cấp một cách dễ dàng thông qua mô-đun setup. Bạn tạo một tập tin có tên là setup.py trong thư mục gốc của gói. Sau đó, để tạo một phân phối nguồn (source distribution), bạn chạy lệnh: python setup.py sdist

Để tạo ra một phân phối nhị phân (binary distribution) gọi là wheel, bạn chạy lệnh: python setup.py bdist_wheel

Đây là tập tin setup.py của pathology package:

1
from setuptools import setup, find_packages
2
3
setup(name='pathology',
4
      version='0.1',
5
      url='https://github.com/the-gigi/pathology',
6
      license='MIT',
7
      author='Gigi Sayfan',
8
      author_email='the.gigi@gmail.com',
9
      description='Add static script_dir() method to Path',
10
      packages=find_packages(exclude=['tests']),
11
      long_description=open('README.md').read(),
12
      zip_safe=False)

Nó bao gồm rất nhiều metadata bên cạnh phần tử 'packages' sử dụng hàm find_packages() được import từ setuptools để tìm ra các gói phụ.

Hãy xây dựng một phân phối nguồn:

1
$ python setup.py sdist
2
running sdist
3
running egg_info
4
creating pathology.egg-info
5
writing pathology.egg-info/PKG-INFO
6
writing dependency_links to pathology.egg-info/dependency_links.txt
7
writing top-level names to pathology.egg-info/top_level.txt
8
writing manifest file 'pathology.egg-info/SOURCES.txt'
9
reading manifest file 'pathology.egg-info/SOURCES.txt'
10
writing manifest file 'pathology.egg-info/SOURCES.txt'
11
warning: sdist: standard file not found: should have one of README, README.rst, README.txt
12
13
running check
14
creating pathology-0.1
15
creating pathology-0.1/pathology
16
creating pathology-0.1/pathology.egg-info
17
copying files to pathology-0.1...
18
copying setup.py -> pathology-0.1
19
copying pathology/__init__.py -> pathology-0.1/pathology
20
copying pathology/path.py -> pathology-0.1/pathology
21
copying pathology.egg-info/PKG-INFO -> pathology-0.1/pathology.egg-info
22
copying pathology.egg-info/SOURCES.txt -> pathology-0.1/pathology.egg-info
23
copying pathology.egg-info/dependency_links.txt -> pathology-0.1/pathology.egg-info
24
copying pathology.egg-info/not-zip-safe -> pathology-0.1/pathology.egg-info
25
copying pathology.egg-info/top_level.txt -> pathology-0.1/pathology.egg-info
26
Writing pathology-0.1/setup.cfg
27
creating dist
28
Creating tar archive
29
removing 'pathology-0.1' (and everything under it)

Cảnh báo là do tôi đã sử dụng tập tin README.md không tiêu chuẩn. Không sao, chúng ta có thể bỏ qua nó. Kết quả là một tập tin tar-gzipped nằm trong thư mục dist:

1
$ ls -la dist
2
total 8
3
drwxr-xr-x   3 gigi.sayfan  gigi.sayfan   102 Apr 18 21:20 .
4
drwxr-xr-x  12 gigi.sayfan  gigi.sayfan   408 Apr 18 21:20 ..
5
-rw-r--r--   1 gigi.sayfan  gigi.sayfan  1223 Apr 18 21:20 pathology-0.1.tar.gz

Và đây là một phân phối nhị phân:

1
$ python setup.py bdist_wheel
2
running bdist_wheel
3
running build
4
running build_py
5
creating build
6
creating build/lib
7
creating build/lib/pathology
8
copying pathology/__init__.py -> build/lib/pathology
9
copying pathology/path.py -> build/lib/pathology
10
installing to build/bdist.macosx-10.7-x86_64/wheel
11
running install
12
running install_lib
13
creating build/bdist.macosx-10.7-x86_64
14
creating build/bdist.macosx-10.7-x86_64/wheel
15
creating build/bdist.macosx-10.7-x86_64/wheel/pathology
16
copying build/lib/pathology/__init__.py -> build/bdist.macosx-10.7-x86_64/wheel/pathology
17
copying build/lib/pathology/path.py -> build/bdist.macosx-10.7-x86_64/wheel/pathology
18
running install_egg_info
19
running egg_info
20
writing pathology.egg-info/PKG-INFO
21
writing dependency_links to pathology.egg-info/dependency_links.txt
22
writing top-level names to pathology.egg-info/top_level.txt
23
reading manifest file 'pathology.egg-info/SOURCES.txt'
24
writing manifest file 'pathology.egg-info/SOURCES.txt'
25
Copying pathology.egg-info to build/bdist.macosx-10.7-x86_64/wheel/pathology-0.1-py3.6.egg-info
26
running install_scripts
27
creating build/bdist.macosx-10.7-x86_64/wheel/pathology-0.1.dist-info/WHEEL

Pathology package chỉ có chứa các mô-đun Python thuần tuý, vì vậy một gói tổng quát có thể được xây dựng. Nếu gói của bạn có các phần mở rộng C, bạn sẽ phải xây dựng một wheel riêng cho mỗi nền tảng:

1
$ ls -la dist
2
total 16
3
drwxr-xr-x   4 gigi.sayfan  gigi.sayfan   136 Apr 18 21:24 .
4
drwxr-xr-x  13 gigi.sayfan  gigi.sayfan   442 Apr 18 21:24 ..
5
-rw-r--r--   1 gigi.sayfan  gigi.sayfan  2695 Apr 18 21:24 pathology-0.1-py3-none-any.whl
6
-rw-r--r--   1 gigi.sayfan  gigi.sayfan  1223 Apr 18 21:20 pathology-0.1.tar.gz

Để tìm hiểu sâu hơn về chủ đề đóng gói thư viện Python, hãy tham khảo bài viết Cách Viết các Gói Python của riêng bạn.

Cách Phân phối một gói Python

Python có một kho lưu trữ gói trung tâm được gọi là PyPI (Python Packages Index). Khi bạn cài đặt một gói Python bằng pip, nó sẽ tải gói từ PyPI (trừ khi bạn chỉ định một kho chứa khác). Để phân phối pathology package của chúng ta, chúng ta cần phải tải nó lên PyPI và cung cấp thêm một số matadata mà PyPI yêu cầu. Các bước là:

  • Tạo một tài khoản trên PyPI (chỉ một lần).
  • Đăng ký gói của bạn.
  • Tải gói của bạn lên.

Tạo một Tài khoản

Bạn có thể tạo một tài khoản trên trang web PyPI. Sau đó tạo một tập tin .pypirc trong thư mục home của bạn:

1
[distutils] 
2
index-servers=pypi
3
 
4
[pypi]
5
repository = https://pypi.python.org/pypi
6
username = the_gigi

Để thử, bạn có thể thêm một máy chủ chỉ mục "pypitest" vào tập tin .pypirc của bạn:

1
[distutils]
2
index-servers=
3
    pypi
4
    pypitest
5
6
[pypitest]
7
repository = https://testpypi.python.org/pypi
8
username = the_gigi
9
10
[pypi]
11
repository = https://pypi.python.org/pypi
12
username = the_gigi

Đăng ký Gói của bạn

Nếu đây là lần xuất bản đầu tiên của gói, bạn cần phải đăng ký nó với PyPI. Sử dụng lệnh register của setup.py. Nó sẽ hỏi mật khẩu của bạn. Lưu ý rằng tôi trỏ nó đến kho thử nghiệm ở đây:

1
$ python setup.py register -r pypitest
2
running register
3
running egg_info
4
writing pathology.egg-info/PKG-INFO
5
writing dependency_links to pathology.egg-info/dependency_links.txt
6
writing top-level names to pathology.egg-info/top_level.txt
7
reading manifest file 'pathology.egg-info/SOURCES.txt'
8
writing manifest file 'pathology.egg-info/SOURCES.txt'
9
running check
10
Password:
11
Registering pathology to https://testpypi.python.org/pypi
12
Server response (200): OK

Tải Gói của bạn lên

Bây giờ thì gói đã được đăng ký, chúng ta có thể tải nó lên. Tôi khuyên bạn nên sử dụng twine, là lựa chọn an toàn hơn. Cài đặt nó như bình thường bằng lệnh pip install twine. Sau đó, tải gói của bạn lên bằng twine và cung cấp mật khẩu của bạn (chỉnh lại lệnh ở bên dưới):

1
$ twine upload -r pypitest -p <redacted> dist/*
2
Uploading distributions to https://testpypi.python.org/pypi
3
Uploading pathology-0.1-py3-none-any.whl
4
[================================] 5679/5679 - 00:00:02
5
Uploading pathology-0.1.tar.gz
6
[================================] 4185/4185 - 00:00:01 

Để tìm hiểu sâu hơn về chủ đề phân phối gói, hãy tham khảo Cách Chia sẻ Gói Python của bạn.

Tóm tắt

Trong hướng dẫn này, chúng ta đã tìm hiểu quy trình đầy đủ trong việc viết một thư viện Python, đóng gói nó và phân phối nó thông qua PyPI. Tại thời điểm này, bạn sẽ có tất cả các công cụ để viết và chia sẻ thư viện của bạn với thế giới.

Ngoài ra, đừng ngại xem thử những gì chúng tôi đang có để bán và để nghiên cứu trên market và vui lòng hỏi bất kỳ câu hỏi nào và cung cấp phản hồi có giá trị của bạn bằng cách sử dụng phần bình luận bên dưới.