برنامه‌نویسی پایتون فهرست مطالب
فصل ۱۷

ماژول‌ها و بسته‌ها

در فصل قبل از ماژول‌های آماده پایتون استفاده کردیم. حالا ماژول خودمان را می‌سازیم و یاد می‌گیریم چطور یک برنامه بزرگ را روی چند فایل پخش کنیم.

در این فصل می‌خوانید
  1. ماژول فقط یک فایل است
  2. وارد کردن با نام مستعار
  3. وارد کردن مستقیم یک نام
  4. وارد کردن چند نام
  5. پوشه __pycache__
  6. بسته چیست
  7. چند ماژول در یک بسته
  8. بازصدور در __init__.py
  9. کلاس داخل بسته
  10. چرا نگهبان اجرا لازم است
  11. ماژول تنظیمات

۱. ماژول فقط یک فایل است

خبر خوب: ماژول چیز خاصی نیست. هر فایل py. که بنویسید، یک ماژول است و می‌شود از فایل دیگری واردش کرد.

'''A tiny module: just a .py file containing functions we want to reuse.'''


def add(num1: float, num2: float) -> float:
    'Return the sum of two numbers.'
    return num1 + num2

فایل math_module.py

# 'import math_module' loads the file math_module.py that sits next to this one.
# The functions are then reached through the module name.
import math_module

if __name__ == '__main__':
    result = math_module.add(num1=5, num2=6)
    print(result)

فایل app.py: نمونه 01_import_module

شرح مثال

۲. وارد کردن با نام مستعار

# A module can be renamed while importing it. This is useful when the real
# name is long (you will see 'import pandas as pd' in real projects).
import math_module as m

if __name__ == '__main__':
    result = m.add(num1=5, num2=6)
    print(result)

نام مستعار: نمونه 02_import_as

در پروژه‌های واقعی نام‌های مستعار قراردادی وجود دارد که همه رعایت می‌کنند: import numpy as np، import pandas as pd، import tkinter as tk. این موارد را در فصل‌های بعد می‌بینید.

۳. وارد کردن مستقیم یک نام

# 'from ... import ...' brings the name directly into our file,
# so we call it without the module prefix.
from math_module import add

if __name__ == '__main__':
    result = add(num1=5, num2=6)
    print(result)

وارد کردن مستقیم: نمونه 03_from_import

این شکل کد را کوتاه‌تر می‌کند ولی یک هزینه دارد: وقتی در وسط فایل add(...) را می‌بینید، معلوم نیست تابع خودتان است یا از جایی آمده است. برای فایل‌های کوچک اشکالی ندارد.

۴. وارد کردن چند نام

# Several names can be imported in one line.
from math_module import add, mul

# 'from math_module import *' would import everything, but then the reader of
# the file no longer knows where 'add' comes from. Avoid it.

if __name__ == '__main__':
    print(add(num1=5, num2=6))
    print(mul(num1=5, num2=6))

چند نام: نمونه 04_import_several

۵. پوشه __pycache__

اولین باری که ماژولی را وارد کنید، پوشه‌ای به نام __pycache__ کنار فایل‌هایتان ظاهر می‌شود. نگران نباشید:

۶. بسته چیست

وقتی ماژول‌ها زیاد شوند، باید دسته‌بندی‌شان کرد. بسته یک پوشه است که داخلش فایلی به نام __init__.py دارد.

05_package/
    app.py
    math_package/
        __init__.py
        my_math.py
# A package is a FOLDER that contains an __init__.py file.
# The path to a module inside it uses dots: package.module
from math_package.my_math import add, mul

if __name__ == '__main__':
    print(add(num1=5, num2=6))
    print(mul(num1=5, num2=6))

استفاده از بسته: نمونه 05_package

شرح مثال

۷. چند ماژول در یک بسته

# Two modules of the same package, each imported with its own path.
from math_package.my_math import add, mul
from math_package.your_math import sub, div

if __name__ == '__main__':
    print(add(num1=5, num2=6))
    print(sub(num1=5, num2=6))
    print(div(num1=5, num2=6))

دو ماژول: نمونه 06_package_two_modules

'''A second module in the same package.'''


def sub(num1: float, num2: float) -> float:
    'Return the difference of two numbers.'
    return num1 - num2


def div(num1: float, num2: float) -> float:
    'Return the quotient of two numbers.'
    if num2 == 0:
        raise ZeroDivisionError('num2 must not be zero')
    return num1 / num2

فایل your_math.py

به تابع div دقت کنید: به جای اینکه پیام خطا چاپ کند، خطا می‌اندازد. این کار درست یک تابع کتابخانه‌ای است. تابع نباید تصمیم بگیرد که پیام خطا را چطور به کاربر نشان دهد؛ آن وظیفه برنامه اصلی است. دستور raise موضوع فصل نوزدهم است.

۸. بازصدور در __init__.py

مشکل بخش قبل این است که کاربر بسته باید بداند هر تابع در کدام ماژول است. با نوشتن چند خط در __init__.py این را حل می‌کنیم.

# __init__.py runs when the package is imported. Re-exporting the names here
# gives the user of the package a single, simple entry point:
#     from math_package import add, sub
#
# The dot in '.my_math' means "the module my_math of THIS package"
# (a relative import).
from .my_math import add, mul
from .your_math import sub, div

فایل __init__.py: نمونه 07_package_init

# Thanks to the re-exports in __init__.py, we no longer have to know
# in which module of the package each function lives.
from math_package import add, mul, sub, div

if __name__ == '__main__':
    print(add(num1=5, num2=6))
    print(div(num1=5, num2=6))

استفاده ساده‌شده: نمونه 07_package_init

شرح مثال

۹. کلاس داخل بسته

'''The Animal class (classes are studied in detail in session 21).'''


class Animal:
    def __init__(self, name: str, weight: int, is_pet: bool = False) -> None:
        self.name = name
        self.weight = weight
        self.is_pet = is_pet

    def __str__(self) -> str:
        return self.name

فایل animals/animal.py: نمونه 08_class_in_a_package

# A package usually exports classes, not only functions.
from animals import Animal

if __name__ == '__main__':
    the_cat = Animal('Tom', 7, is_pet=True)
    print(the_cat)

    the_wolf = Animal('Fang', 20)
    print(the_wolf)

استفاده از کلاس: نمونه 08_class_in_a_package

کلاس موضوع فصل بیست و یکم است، ولی نکته این بخش ربطی به کلاس ندارد: قرارداد رایج این است که هر کلاس در فایل خودش باشد و __init__.py آن را بازصادر کند. در پروژه پایانی فصل سی و نهم دقیقاً همین ساختار را می‌بینید.

۱۰. چرا نگهبان اجرا لازم است

حالا می‌فهمیم آن خط if __name__ == '__main__': که از فصل دوازدهم می‌نویسیم واقعاً به چه درد می‌خورد.

'''This module can be RUN on its own and also be IMPORTED.'''


class Animal:
    def __init__(self, name: str, weight: int, is_pet: bool = False) -> None:
        self.name = name
        self.weight = weight
        self.is_pet = is_pet

    def __str__(self) -> str:
        return self.name


# Everything below runs only when this file is started directly
# (python animals/animal.py). Without this guard, the two animals would also
# be created and printed every time app.py imports the module.
if __name__ == '__main__':
    the_cat = Animal('Tom from the module', 7)
    print(the_cat)

    the_wolf = Animal('Fang from the module', 20)
    print(the_wolf)

نگهبان اجرا در ماژول: نمونه 09_module_main_guard

خودتان امتحان کنید

اول python animals/animal.py را اجرا کنید: کد آزمایشی ماژول اجرا می‌شود. بعد python app.py را اجرا کنید: فقط حیوان‌های خود app.py چاپ می‌شوند. حالا خط نگهبان را بردارید و دوباره app.py را اجرا کنید. می‌بینید که حیوان‌های ماژول هم چاپ می‌شوند، در حالی که کسی آنها را نخواسته بود.

این یعنی هر ماژولی می‌تواند کد آزمایشی خودش را داشته باشد بدون اینکه مزاحم کسی شود. عادت خوبی است که هر ماژولی که می‌نویسید، در انتهایش یک بلوک نگهبان با چند نمونه استفاده داشته باشد.

۱۱. ماژول تنظیمات

یک کاربرد بسیار عملی ماژول: همه مقدارهای ثابت برنامه را در یک فایل جدا بگذارید.

'''All the settings of the program in one place.

Constants are written in CAPITALS. Keeping them in their own module means that
changing a message or a limit never requires touching the logic of the program.
'''

APP_NAME: str = 'Kadoos Shop'
CURRENCY: str = 'Toman'

VAT_RATE: float = 0.09
MAX_ITEMS: int = 10

WELCOME_MESSAGE: str = 'Welcome to {app}!'
GOODBYE_MESSAGE: str = 'Thank you, see you soon.'

PRODUCTS: dict[str, int] = {
    'Notebook': 15_000,
    'Pencil': 5_000,
    'Eraser': 3_000,
}

فایل constants.py: نمونه 10_constants_module

'''A small shop that reads every setting from constants.py.'''

import constants


def show_products() -> None:
    'Print the price list.'
    for name, price in constants.PRODUCTS.items():
        print(f'{name:<10}{price:>10,} {constants.CURRENCY}')


def price_with_vat(price: float) -> float:
    'Return the price including VAT.'
    return price * (1 + constants.VAT_RATE)


def main() -> None:
    print(constants.WELCOME_MESSAGE.format(app=constants.APP_NAME))
    show_products()

    product = input('Which product do you want? ')

    if product in constants.PRODUCTS:
        total = price_with_vat(constants.PRODUCTS[product])
        print(f'With VAT: {total:,.0f} {constants.CURRENCY}')
    else:
        print('We do not sell that.')

    print(constants.GOODBYE_MESSAGE)


if __name__ == '__main__':
    main()

استفاده از تنظیمات: نمونه 10_constants_module

شرح مثال

یک هشدار امنیتی

رمز عبور، توکن ربات و کلید API را هرگز در constants.py نگذارید، چون آن فایل معمولاً در گیت ذخیره می‌شود و روی اینترنت می‌رود. این موارد باید از متغیر محیطی خوانده شوند. در فصل سی و هفتم که ربات پیام‌رسان می‌نویسیم، دقیقاً روی همین موضوع مکث می‌کنیم، چون در نسخه اصلی این دوره یک توکن واقعی داخل کد رها شده بود.

فایل‌های تمرینی این فصل

پوشه S17-modules-and-packages شامل ده پوشه نمونه، از 01_import_module تا 10_constants_module. هر کدام را باید از داخل خودش اجرا کنید.

خلاصه فصل

موردتوضیح
ماژولهر فایل py.
بستهپوشه‌ای که __init__.py دارد
import mوارد کردن کامل، استفاده با m.name
import m as xنام مستعار
from m import a, bوارد کردن مستقیم چند نام
from p.m import aماژول داخل بسته، با نقطه
from .m import aایمپورت نسبی، داخل خود بسته
__init__.pyهنگام وارد کردن بسته اجرا می‌شود. جای بازصدور
__pycache__نسخه ترجمه‌شده. قابل پاک کردن، نباید در گیت باشد
نگهبان اجراکد آزمایشی ماژول هنگام وارد شدن اجرا نشود
constants.pyهمه تنظیمات ثابت در یک جا. بدون رمز و توکن

تمرین‌های فصل هفده

  1. ماژول geometry.py با توابع circle_area و rectangle_area و triangle_area بسازید و از یک app.py استفاده‌اش کنید.
  2. همان ماژول را به هر سه شکل وارد کنید و مقایسه کنید.
  3. geometry.py را به بسته geometry/ با دو ماژول flat.py و solid.py تبدیل کنید.
  4. در __init__.py آن بسته، همه چیز را بازصادر کنید.
  5. به هر ماژولتان یک بلوک آزمایشی با نگهبان اجرا اضافه کنید و ببینید هنگام وارد شدن اجرا نمی‌شود.
  6. برای یکی از برنامه‌های قبلی‌تان یک constants.py بسازید و همه متن‌ها و عددهای ثابت را به آن منتقل کنید.
  7. مقدار __name__ را هم در ماژول و هم در برنامه‌ای که آن را وارد می‌کند چاپ کنید و تفاوت را توضیح دهید.
فصل قبل: کتابخانه استاندارد فصل بعد: فایل‌ها و سیستم‌عامل