การสร้าง module และ package ใน python

โมดูลและแพคเกจเป็นหลักการที่สำคัญของ python และโปรแกรมภาษาอื่นๆ เมื่อเราพัฒนาโปรแกรมขนาดใหญ่ขึ้นการแยก code เป็นส่วนๆจึงเป็นความจำเป็นโดยธรรมชาติ หากเราแยก code ที่มีฟังก์ชั่นเดียวกันไว้เป็นไฟล์เราจะได้สิ่งที่เรียกว่าโมดูล-module หากเราเอาโมดูลหลายโมดูลมารวมกันในไดเรคทอรี่หนึ่งเราจะได้แพคเกจ-package เมื่อแยก code เป็นโมดูลและแพคเกจแล้วเราก็จะต้องมีวิธีการที่จะ import เข้ามา

Module คืออะไร

เมื่อเราเขียนโปรแกรมเพื่อใช้ประโยชน์อย่างจริงจังโปรแกรมจะมีขนาดใหญ่โดยอัตโนมัติซึ่งยากแก่การพัฒนาต่อหากทั้งหมดรวมอยู่ในไฟล์เดียว จึงมีความจำเป็นต้องแยก code ออกเป็นส่วนๆตามจุดประสงค์ที่คล้ายกัน ในทางปฏิบัติเราจะแยกออก code แต่ละส่วนออกมาเป็นไฟล์ซึ่งแต่ละไฟล์เรียกว่า module เมื่อแยกออกมาแล้ว code หลักก็จะต้องอ้างถึงคำสั่งหรือข้อมูลที่อยู่ในไฟล์หรือโมดูลนั้นด้วยคำสั่ง import เราคุ้นเคยกับคำสั่งนี้มาตั้งแต่เริ่มต้นเขียนโปรแกรมด้วย python แล้ว สิ่งที่เรา import เข้ามาก็คือ module นั่นเอง

ภายในโมดูลประกอบไปด้วย object ต่างๆที่ code ส่วนอื่นๆจะเรียกใช้ คำว่า object ในที่นี้ไม่ได้หมายถึง instance ของ class ในภาษา OOP เท่านั้นแต่หมายถึงตัวแปรข้อมูลทุกชนิด(ซึ่งโดยปกติในภาษา OOP จะมองว่าเป็น object ทั้งหมด) รวมทั้งการนิยาม class ที่อยู่ในไฟล์หรือโมดูลก็จะเรียก(ในที่นี้)ว่าเป็น object ด้วย

Package คืออะไร

หากโปรแกรมมีขนาดใหญ่มากเราย่อมจะแบ่งออกเป็นหลายโมดูล หลายๆโมดูลที่มีวัตถุประสงค์เดียวกันแล้วเอามาอยู่รวมกันเราเรียกว่าเป็นหนึ่ง package ในทางปฎิบัติหนึ่งแพคเกจก็คือหนึ่งโฟลเดอร์หรือหนึ่งไดเรคทอรี่นั่นเองเหมือนกับที่เรารวมไฟล์หลายไฟล์ไว้ในไดเรคทอรี่ ชื่อไดเรคทอรี่ก็คือชื่อ package และหลายๆแพคเกจก็สามารถรวมกันอยู่ในอีกไดเรคทอรี่หรือแพคเกจอื่นได้เช่นกันหาก project ของเรามีขนาดใหญ่มาก

จะแยกเป็น Module และ Package อย่างไร

การแยก code ออกมาเป็นโมดูลเป็นสิ่งที่คุณในฐานะของผู้เขียนโปรแกรมต้องพิจารณาเองว่าควรจะแยกเป็นกี่ไฟล์หรือกี่โมดูล และควรจะแยกเป็น package ไหม แล้วควรจะมีกี่ package และควรมี package ซ้อนไหม

การแยกเป็น module อย่างเดียว

โครงสร้างอย่างง่ายที่สุดคือการแยกเป็นโมดูลแล้วให้อยู่ในไดเรคทอรี่เดียวกันกับตัว code หลัก เช่น

#main.py
from numpy import sqrt
def square(x):
        return x**2
def sqrt(x):
        return sqrt(x)
print( square( 2 ) )
print( sqrt(2) )

แยกเป็นโมดูล simplemath.py

#simplemath.py
from numpy import sqrt
def square(x):
        return x**2
def sqrt(x):
        return sqrt(x)

และตัว code หลักแก้เป็น new_main.py

#new_main.py
from simplemath import square, sqrt
print( square( 2 ) )
print( sqrt(2) )

โดยให้ simplemath.py และ new_main.py อยู่ในไดเรคทอรี่เดียวกัน

การแยกเป็น module และ package

การสร้าง package เฉพาะกิจ

หากเรามีหลายโมดูลการเอาโมดูลเหล่านั้นมารวมไว้ในไดเรคทอรี่เดียวกันจะสะดวกแก่การดูแลมากกว่า สมมติว่าเราเอา package ของเราไปใส่ไว้ในไดเรคทอรี่ my_packages ซึ่งประกอบด้วยโมดูล sqrt.py ซึ่งคำนวณรากที่สองและโมดูล square.py ซึ่งคำนวณกำลังสอง

การสร้าง package อย่างง่าย

โดยถูก import เข้ามาและเรียกใช้ใน main.py

#main.py
from my_packages.sqrt import sqrt
from my_packages.square import square
print( sqrt(4) )
print( square(3) )

บรรทัดที่ 1 my_packages.sqrt หมายถึงไฟล์ sqrt.py ที่อยู่ในไดเรกทอรี่ mypackages บรรทัดที่ 2 my_packages.square หมายถึงไฟล์ square.py ที่อยู่ในไดเรกทอรี่เดียวกัน

#sqrt.py
import numpy as np
def sqrt(x):
      return np.sqrt(x)
#square.py
def square(x):
      return x*x

ซึ่งต่างก็มี object ที่ต้องการ import เข้ามาใน main.py ซึ่งก็คือ sqrt และ square ดังนั้นคำสั่ง

from my_packages.sqrt import sqrt

จึงหมายถึงว่าต้องการใช้ฟังก์ชั่น sqrt ที่อยู่ในไฟล์ sqrt.py ที่อยู่ในไดเรกทอรี่ my_packages ที่อยู่ในระดับเดียวกันกับ main.py หรือโปรแกรมที่ออกคำสั่งนี้ ส่วน from my_packages.sqrt import square ก็มีความหมายแบบเดียวกัน

การสร้าง package ด้วย __init__.py

หากโปรเจ็คท์ของเรามีขนาดใหญ่ขึ้นการสร้างแพคเกจแบบแรกจะบำรุงรักษายาก วิธีนี้ใน main.py การ import จะเปลี่ยนเป็น

main.py
from my_packages import sqrt
from my_packages import square
print( sqrt(4) )
print( square(3) )

วิธีนี้จะใช้คำสั่ง from my_packages ซึ่งเป็นการอ้างถึงไดเรกทอรี่ซึ่งไม่ได้ระบุชื่อไฟล์เหมือนแบบแรก จากนั้นก็ import สิ่งที่ต้องการเหมือนกัน แต่จะรู้ได้อย่างไรว่า sqrt และ square อยู่ในไฟล์อะไรภายใต้ไดเรกทอรี่ my_packages

Python ให้เราสร้างไฟล์ __init__.py ไว้ภายใต้ไดเรกทอรี่ my_packages เพื่อใช้เป็นตัวบอกว่า object ที่อ้างถึง(sqrt และ square ในที่นี้) อยู่ในไฟล์อะไร

ไฟล์ __init__.py เป็นแผนที่บอกที่อยู่ object ที่ต้องการ

ในกรณีนี้ให้ใส่คำสั่ง

__init__.py
from .sqrt import sqrt
from .square import square

โดย .sqrt หมายถึงไฟล์ sqrt.py คำสั่ง from .sqrt import sqrt มีหมายความว่า object ที่ชื่อ sqrt อยู่ภายในไฟล์ sqrt.py และ from .square import square ก็มีความหมายแบบเดียวกัน

การสร้าง package ซ้อน package

สมมติว่าเรามีแพคเกจ sub_package อยู่ภายใต้แพคเกจ my_package อีกที และใน main.py เราต้องการอ้างอิงถึง object ที่อยู่ในแพคเกจย่อยนี้

โครงสร้างของ package ซ้อน package
#main.py
from my_packages import sqrt
from my_packages import square
from my_packages.sub_package.cube import cube, pythagoras
print(sqrt(4))
print(square(3))
print(cube(2))
print(pythagoras(2, 3))

คำสั่งที่ 2 และ 3 เป็นการ import object ชื่อ sqrt และ square ภายใน my_packages ตามปกติ คำสั่งที่ 4 เป็นการ import object cube และ pythagorus ซึ่งอยู่ในแพคเกจ sub_package สังเกตว่าจะใช้ชื่อแพคเกจตามตามด้วย . และตามด้วยชื่อแพคเกจย่อย และหากมีย่อยลงไปอีกก็ใช้ . ต่อไปเรื่อยๆเช่นกัน

#cube.py
def cube(x):
     return x*x*x
#pythagoras.py
def pythagoras(x,y):
     return x**2 + y**2

การสร้าง package ที่อยู่เหนือขึ้นไป

ในงานขนาดใหญ่ที่ประกอบไปด้วยหลายๆ project มักมีการใช้ package ร่วมกัน การเอา package ที่ต้องการใช้ร่วมกันไปใส่ใน project หนึ่งจะทำให้การอ้างถึงไม่สะดวกกับ project อื่น วิธีที่ดีคือแยก package นั้นออกมาอยู่ข้างนอกเพื่อให้ทุก project เรียกใช้ในแบบเดียวกัน

สมมติโปรเจ็คท์ตามตัวอย่างก่อนหน้าอยู่ภายใต้ไดเรกทอรี่ math แล้วเรามีโปรเจ็คท์อื่นๆที่อยู่ระดับเดียวกันคือ data_analysis และ machine_learning สมมติว่าทั้ง 3 โปรเจ็คต้องใช้แพคเกจ upper_packages ร่วมกัน ซึ่งจะเห็นว่าแพคเกจนี้ไม่ได้อยู่ภายในโปรเจ็คท์ใดโปรเจ็คท์หนึ่ง การ import ตามวิธีที่ผ่านมาจึงใช้ไม่ได้

upper_packages อยู่เหนือ main.py

เราจะต้องเซตไดเรคทอรี่เริ่มต้นสำหรับการค้นหาแพคเกจเสียก่อน สมมติว่าใน main.py ต้องการ import data ซึ่งเป็น object ใน data.py

#main.py
import os, sys
currentdir = os.path.dirname(os.path.realpath(__file__))
parentdir = os.path.dirname(currentdir)
sys.path.append(parentdir)
from upper_packages.data import data
print(data)

คำสั่ง 1 บอกว่าต้องการใช้ os และ sys ซึ่งเกี่ยวข้องกับไฟล์และ path
คำสั่ง 2 เป็นการแยกเอาเฉพาะชื่อไดเรคทอรี่ออกมาจากชื่อไฟล์ เช่น os.path.dirname(‘/dir1/dir2/main.py’) จะได้เอาท์พุทเป็น dir1/dir2 , __file__ หมายถึงชื่อไฟล์ที่กำลังรันอยู่ในที่นี้ก็คือ main.py , os.path.realpath(__file__) หมายถึง path + ชื่อไฟล์ที่กำลังรันซึ่งก็คือ /home/user/…/main.py บน linux หรือ c:\…\main.py
คำสั่ง 3 เมื่อคร่อมด้วย os.path.dirname จึงหมายถึง /home/user/…/ หรือ c:…\ ซึ่งเป็น path ของ main.py ที่กำลังรันอยู่
คำสั่ง 4 sys.path.append เป็นการเซตให้ใช้ path นี้เป็นจุดเริ่มต้นในการค้นหาแพคเกจจากคำสั่ง from
คำสั่ง 5 from upper_packages.data จึงเป็นการสั่งให้หาแพคเกจ upper_packages ภายใต้เอาท์พุทจากคำสั่ง 3

การสร้างแพคเกจไว้ใน pipy.org

ที่ผ่านมาเป็นการสร้างแพคเกจไว้ในเครื่องของเราซึ่งเราอาจใช้คนเดียวหรือเป็นโปรเจ็คภายในที่ไม่ต้องการให้มีการแชร์สู่โลกภายนอก แต่ถ้าเราต้องการแชร์ก็สามารถนำแพคเกจไปวางไว้ใน server สำหรับเก็บแพคเกจ python ซึ่งก็คือ pypi.org เมื่อ upload ไปแล้วเราก็สามารถ install แบบเดียวกับแพคเกจอื่นๆที่เคยเคยทำ

pip install mypackage

pypi.org ไม่ได้เก็บแพคเกจของเราโดยตรงแต่ทำงานร่วมกับ github.com โดยอย่างหลังจะทำหน้าที่เก็บแพคเกจซึ่งเราจะต้อง upload ไปไว้ที่นี่ เริ่มแรกคุณจะต้องสมัครสมาชิกของทั้งสองเว็บไซต์ก่อน สมัคร github ได้ที่หน้าแรก สมัคร pypi คลิกที่นี่

Github.com

เขาจะให้เราใส่แพคเกจของเราไว้ในคลังของเขาซึ่งเรียกว่า repository เราสามารถสร้างได้หลายคลังโดยการกดปุ่ม New repository โดยแต่ละ repository จะมี url เป็น username/repository เช่น username คือ moderncoursesquare แล้วสร้าง repository ชื่อ simplemath ก็จะได้ url ของ repository เป็น moderncoursesquare/simplemath ซึ่งจะเป็นที่ที่เราจะ upload แพคเกจของเราขึ้นไป

pypi.org

เราจะต้องสร้างแพคเกจในเครื่องของเราซึ่งเป็นไดเรคทอรี่หนึ่ง จากนั้นสร้างไฟล์ชื่อ setup.py ไว้ในไดเรคทอรี่นี้

import pathlib
 from setuptools import setup
 The directory containing this file
 HERE = pathlib.Path(__file__).parent
 The text of the README file
 README = (HERE / "README.md").read_text()
 This call to setup() does all the work
 setup(
     name="mcs-packages",
     version="1.0.0",
     description="Demo for creating packages on pypi.org",
     long_description=README,
     long_description_content_type="text/markdown",
     url="https://github.com/moderncoursesquare/pypi-packages/my_packages/",
     author="moderncoursesquare",
     author_email="moderncoursesquare@gmail.com",
     license="MIT",
     classifiers=[
         "License :: OSI Approved :: MIT License",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.8",
     ],
     packages=["mcs_packages"],
     include_package_data=True,
     install_requires=[],
     entry_points={
         "console_scripts": [
             "mcs=samplepackage.__main__:main",
         ]
     },
 )

คำสั่ง name=”mcs_mathplotlib” คือชื่อแพคเกจของเรา(ซึ่งก็คือชื่อไดเรคทอรี่) version=”1.0.1″ เราจะต้องใส่ เมื่อเรามีการปรับปรุง code เราก็จะต้องปรับตัวเลขนี้ขึ้นไปเรื่อยไและ upload ขึ้นไปใหม่ url=”https://github.com/moderncoursesquare/pythonlib”, เอามาจาก github ซึ่งเราเก็บแพคเกจของเราไว้ packages=[“mcs_mathplotlib”] เป็นชื่อเดียวกับแพคเกจ

จากนั้นเราจะต้อง compile โปรแกรม setup.py ของเราด้วยคำสั่ง

python setup.py  sdist

ซึ่งเราจะได้ไฟล์และไดเรคทอรี่สร้างขึ้นมาภายใต้ไดเรคทอรี่แพคเกจของเราซึ่งเราจะ upload เหล่านี้ขึ้นไปที่ pypy.org ผ่านโปรแกรม twine ซึ่ง install ด้วยคำสั่ง

pip install twine

จากนั้นเปิด terminal/command line ภายใต้ไดเรคทอรี่แพคเกจของเรา(ซึ่งบรรจุไฟล์จากการ compile ที่ทำมาข้างต้น)แล้วใช้คำสั่ง twine

twine upload dist/*

โดยเราจะต้องใส่ username/password ที่เราสมัครไว้กับ pypi.org เข้าไป ซี่งเสร็จแล้วเราจะได้แพคเกจของเราอยู่ที่ https://pypi.org/manage/projects/ ตามตัวอย่างนี้คือ mcs-matplotlib ซึ่งเราสามารถติดตั้งผ่านคำสั่ง

pip install mcs-matplotlib