Mục con


6. Mô-đun

Nếu bạn thoát khỏi trình thông dịch và chạy nó lại, những gì bạn đã định nghĩa (hàm và biến) đều bị mất. Do đó, nếu bạn muốn viết một chương trình dài hơn, thì tốt nhất bạn nên dùng một trình soạn thảo để chuẩn bị đầu vào cho trình thông dịch và chạy nó với tập tin vào này. Việc này được gọi là tạo kịch bản (script). Khi chương trình của bạn trở nên dài hơn, bạn sẽ muốn tách nó ra thành nhiều tập tin để dễ duy trì. Bạn sẽ muốn dùng một hàm thuận tiện mà bạn đã viết trong nhiều chương trình mà không cần phải chép lại định nghĩa của nó vào các chương trình đó.

Để hỗ trợ việc này, Python có một cách đặt các định nghĩa vào một tập tin và dùng chúng trong một kịch bản hoặc trong một phiên làm việc với trình thông dịch. Tập tin này được gọi là mô-đun (module); các định nghĩa từ một mô-đun có thể được nhập (import) vào các mô-đun khác hoặc vào mô-đun chính (tập hợp các biến mà bạn có thể truy cập tới trong một kịch bản được chạy ở cấp cao nhất và trong chế độ máy tính).

Mô-đun là một tập tin chứa các định nghĩa và câu lệnh Python. Tên tập tin là tên của mô-đun với đuôi .py được gắn vào. Trong một mô-đun, tên của mô-đun (là một chuỗi) có thể được truy cập qua một biến toàn cục __name__. Ví dụ, dùng trình soạn thảo của bạn để tạo một tập tin đặt tên là fibo.py trong thư mục hiện tại với nội dung sau:

# Fibonacci numbers module

def fib(n):    # write Fibonacci series up to n
    a, b = 0, 1
    while b < n:
        print b,
        a, b = b, a+b

def fib2(n): # return Fibonacci series up to n
    result = []
    a, b = 0, 1
    while b < n:
        result.append(b)
        a, b = b, a+b
    return result

Bây giờ chạy trình thông dịch Python và nhập mô-đun này với dòng lệnh sau:

>>> import fibo

Việc này sẽ không nhập trực tiếp các tên hàm định nghĩa trong fibo vào bảng ký hiệu (symbol table); nó chỉ nhập tên mô-đun fibo mà thôi. Dùng tên mô-đun bạn có thể truy cập các hàm:

>>> fibo.fib(1000)
1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> fibo.fib2(100)
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> fibo.__name__
'fibo'

Nếu bạn định dùng một hàm thường xuyên, thì bạn có thể gán nó vào một tên cục bộ:

>>> fib = fibo.fib
>>> fib(500)
1 1 2 3 5 8 13 21 34 55 89 144 233 377


6.1 Bàn thêm về mô-đun

Mô-đun có thể chứa các câu lệnh khả thi cũng như các định nghĩa hàm. Các câu lệnh này nhằm mục đích khởi tạo mô-đun. Chúng sẽ chỉ được chạy lần đầu mô-đun được nhập ở đâu đó.6.1

Mỗi mô-đun có một bảng ký hiệu riêng của nó và được dùng như bảng toàn cục đối với mọi hàm được định nghĩa trong mô-đun. Do đó, tác giả của một mô-đun có thể sử dụng các biến toàn cục trong mô-đun mà không phải lo lắng về việc trùng lặp với các biến toàn cục của người dùng. Mặt khác, nếu bạn biết bạn đang làm gì, bạn có thể truy cập vào các biến toàn cục của mô-đun với cùng một cách dùng để truy cập các hàm của nó. modname.itemname.

Mô-đun có thể nhập các mô-đun khác. Thông thường (nhưng không bắt buộc) ta hay để tất cả các lệnh import ở đầu một mô-đun (hoặc kịch bản). Các tên của mô-đun bị nhập được đặt trong bảng ký hiệu toàn cục của mô-đun nhập nó.

Có một biến thể của câu lệnh import để nhập nhiều tên trực tiếp từ một mô-đun vào trong bảng ký hiệu của mô-đun nhập. Ví dụ:

>>> from fibo import fib, fib2
>>> fib(500)
1 1 2 3 5 8 13 21 34 55 89 144 233 377

Câu lệnh này không đưa tên mô-đun bị nhập vào bảng ký hiệu cục bộ (do đó trong ví dụ này, fibo chưa được định nghĩa)

Và một biến thể khác để nhập tất cả các tên từ một mô-đun:

>>> from fibo import *
>>> fib(500)
1 1 2 3 5 8 13 21 34 55 89 144 233 377

Câu lệnh này nhập tất cả mọi tên trừ những tên bắt đầu bằng dấu gạch chân (_).


6.1.1 Đường dẫn tìm mô-đun

Khi mô-đun tên spam được nhập vào, trình thông dịch sẽ tìm một tập tin tên spam.py trong thư mục hiện tại, rồi trong danh sách các thư mục được chỉ định bởi biến môi trường PYTHONPATH. Biến này có cùng cú pháp như là biến môi trường PATH, cùng chứa một danh sách tên các thư mục. Khi PYTHONPATH chưa được thiết lập, hoặc khi tập tin không được tìm thấy, việc tìm kiếm sẽ tiếp tục tìm trong đường dẫn mặc định tùy theo khi cài Python; trên Unix, đường dẫn này thường là .:/usr/local/lib/python.

Thật ra, mô-đun được tìm trong danh sách các thư mục chỉ định bởi biến sys.path đã được thiết lập từ thư mục chứa kịch bản nguồn (hoặc thư mục hiện tại), PYTHONPATH và các mặc định khi cài đặt. Điều này cho phép các chương trình Python thay đổi hoặc thay thế đường dẫn tìm mô-đun. Lưu ý rằng vì thư mục chứa kịch bản đang chạy nằm trong đường dẫn tìm kiếm, tên của kịch bản nhất thiết phải không trùng với tên các mô-đun chuẩn, nếu không thì Python sẽ cố sức nạp kịch bản như là một mô-đun khi mô-đun đó được nhập vào. Thông thường điều này sẽ gây lỗi. Xem mục 6.2, ``Các mô-đun chuẩn,'' để biết thêm chi tiết.

6.1.2 Các tập tin Python ``đã dịch''

Như một cách quan trọng để tăng tốc quá trình khởi động của các chương trình ngắn có dùng nhiều mô-đun chuẩn, nếu tập tin có tên spam.pyc tồn tại trong thư mục mà spam.py được tìm thấy, tập tin này được giả định là phiên bản đã được ``biên dịch byte'' (byte-compile) của mô-đun spam. Thời gian thay đổi của phiên bản spam.py dùng để tạo spam.pyc được lưu lại trong spam.pyc, và tập tin .pyc sẽ bị bỏ qua nếu chúng không khớp nhau.

Thông thường, bạn không cần làm gì cả để tạo tập tin spam.pyc . Khi nào spam.py được biên dịch thành công, Python sẽ thử ghi phiên bản đã biên dịch ra spam.pyc. Nếu việc ghi này thất bại thì cũng không có lỗi gì xảy ra; nếu vì lý do gì đó mà tập tin không được ghi đầy đủ, tập tin spam.pyc sẽ bị đánh dấu là không hợp lệ và sẽ bị bỏ qua sau này. Nội dung của tập tin spam.pyc không phụ thuộc vào hệ thống, do đó một thư mục mô-đun Python có thể được chia xẻ với nhiều máy trên các kiến trúc khác nhau.

Một vài mẹo cho chuyên gia:


6.2 Các mô-đun chuẩn

Python có một thư viện các mô-đun chuẩn, được nói tới trong một tài liệu khác, Tham khảo thư viện Python (từ nay gọi là ``Tài liệu tham khảo'''). Một vài mô-đun được chuyển thẳng vào trình thông dịch; chúng cung cấp các tác vụ không nằm trong phạm vi chính của ngôn ngữ nhưng cũng được tạo sẵn vì hiệu quả cao hoặc để truy cập vào những chức năng của hệ điều hành ví dụ như các lệnh gọi hệ thống. Tập hợp các mô-đun này là một tùy chọn cấu hình lệ thuộc vào hệ thống. Ví dụ, mô-đun amoeba chỉ có trên các hệ hỗ trợ Amoeba. Cần phải nhắc đến mô-đun: sys, được đưa vào trong mọi trình thông dịch Python. Các biến sys.ps1sys.ps2 định nghĩa những chuỗi được dùng như dấu nhắc chính và phụ:

>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print 'Yuck!'
Yuck!
C>

Hai biến này chỉ được định nghĩa khi trình thông dịch chạy ở chế độ tương tác.

Biến sys.path là một danh sách các chuỗi quyết định đường dẫn tìm kiếm các mô-đun của trình thông dịch. Nó được khởi tạo theo đường dẫn mặc định từ biến môi trường PYTHONPATH, hoặc từ một giá trị có sẵn nếu PYTHONPATH không được thiết lập. Bạn có thể sửa nó bằng cách dùng các công cụ trên danh sách:

>>> import sys
>>> sys.path.append('/ufs/guido/lib/python')


6.3 dir() hàm

Hàm có sẵn dir() được dùng để tìm các tên một mô-đun định nghĩa. Nó trả về một danh sách các chuỗi đã sắp xếp:

>>> import fibo, sys
>>> dir(fibo)
['__name__', 'fib', 'fib2']
>>> dir(sys)
['__displayhook__', '__doc__', '__excepthook__', '__name__', '__stderr__',
 '__stdin__', '__stdout__', '_getframe', 'api_version', 'argv', 
 'builtin_module_names', 'byteorder', 'callstats', 'copyright',
 'displayhook', 'exc_clear', 'exc_info', 'exc_type', 'excepthook',
 'exec_prefix', 'executable', 'exit', 'getdefaultencoding', 'getdlopenflags',
 'getrecursionlimit', 'getrefcount', 'hexversion', 'maxint', 'maxunicode',
 'meta_path', 'modules', 'path', 'path_hooks', 'path_importer_cache',
 'platform', 'prefix', 'ps1', 'ps2', 'setcheckinterval', 'setdlopenflags',
 'setprofile', 'setrecursionlimit', 'settrace', 'stderr', 'stdin', 'stdout',
 'version', 'version_info', 'warnoptions']

Không có thông số, dir() liệt kê các tên bạn đã định nghĩa:

>>> a = [1, 2, 3, 4, 5]
>>> import fibo
>>> fib = fibo.fib
>>> dir()
['__builtins__', '__doc__', '__file__', '__name__', 'a', 'fib', 'fibo', 'sys']

Lưu ý rằng nó liệt kê mọi loại tên: biến, mô-đun, hàm, v.v...

dir() không liệt kê tên của các hàm và biến có sẵn. Nếu bạn muốn có danh sách của chúng, thì chúng được định nghĩa trong mô-đun chuẩn __builtin__:

>>> import __builtin__
>>> dir(__builtin__)
['ArithmeticError', 'AssertionError', 'AttributeError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
 'FloatingPointError', 'FutureWarning', 'IOError', 'ImportError',
 'IndentationError', 'IndexError', 'KeyError', 'KeyboardInterrupt',
 'LookupError', 'MemoryError', 'NameError', 'None', 'NotImplemented',
 'NotImplementedError', 'OSError', 'OverflowError', 
 'PendingDeprecationWarning', 'ReferenceError', 'RuntimeError',
 'RuntimeWarning', 'StandardError', 'StopIteration', 'SyntaxError',
 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'True',
 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError',
 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError',
 'UserWarning', 'ValueError', 'Warning', 'WindowsError',
 'ZeroDivisionError', '_', '__debug__', '__doc__', '__import__',
 '__name__', 'abs', 'apply', 'basestring', 'bool', 'buffer',
 'callable', 'chr', 'classmethod', 'cmp', 'coerce', 'compile',
 'complex', 'copyright', 'credits', 'delattr', 'dict', 'dir', 'divmod',
 'enumerate', 'eval', 'execfile', 'exit', 'file', 'filter', 'float',
 'frozenset', 'getattr', 'globals', 'hasattr', 'hash', 'help', 'hex',
 'id', 'input', 'int', 'intern', 'isinstance', 'issubclass', 'iter',
 'len', 'license', 'list', 'locals', 'long', 'map', 'max', 'min',
 'object', 'oct', 'open', 'ord', 'pow', 'property', 'quit', 'range',
 'raw_input', 'reduce', 'reload', 'repr', 'reversed', 'round', 'set',
 'setattr', 'slice', 'sorted', 'staticmethod', 'str', 'sum', 'super',
 'tuple', 'type', 'unichr', 'unicode', 'vars', 'xrange', 'zip']


6.4 Gói

Gói (package) là một cách để cấu trúc vùng tên mô-đun của Python bằng cách dùng ``tên mô-đun có chấm''. Ví dụ, tên mô-đun A.B chỉ ra mô-đun con tên "B" trong một gói tên "A". Cũng như việc sử dụng mô-đun giúp tác giả của các mô-đun khác nhau không phải lo lắng về các biến toàn cục của nhau, việc sử dụng tên mô-đun có chấm giúp tác giả của các gói đa mô-đun như NumPy hay Python Imaging Library không phải lo lắng về tên mô-đun của họ.

Giả sử bạn muốn thiết kế một tập hợp các mô-đun (một ``gói'') nhằm vào việc xử lý các tập tin và dữ liệu âm thanh. Có nhiều định dạng tập tin âm thanh (thường được nhận dạng dựa vào phần mở rộng, ví dụ: .wav, .aiff, .au), do đó bạn sẽ cần tạo và duy trì một tập hợp luôn tăng của các mô-đun cho việc chuyển đổi giữa các định dạng khác nhau. Cũng có nhiều tác vụ khác nhau bạn muốn thực hiện với dữ liệu âm thanh (ví dụ như tổng hợp, thêm tiếng vang, áp dụng chức năng làm bằng, tạo ra hiệu ứng nổi), do đó bạn sẽ không ngừng viết một loạt các mô-đun để thực hiện các tác vụ này. Sau đây là một cấu trúc minh họa cho gói của bạn (được trình bày theo cách của một hệ tập tin phân cấp)

Sound/                          Top-level package
      __init__.py               Initialize the sound package
      Formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      Effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      Filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

Khi nhập một gói, Python tìm trong các thư mục từ sys.path để tìm thư mục con của gói.

Các tập tin __init__.py là cần thiết để cho Python biết các thư mục chứa các gói; việc này được đặt ra để tránh các thư mục với tên chung, ví dụ như "string", vô tình che mất mô-đun hợp lệ xuất hiện sau trong đường dẫn tìm kiếm. Trong trường hợp đơn giản nhất, __init__.py có thể chỉ là một tập tin rỗng, nhưng nó cũng có thể thực thi các mã thiết lập của gói hoặc thiết lập biến __all__ , sẽ được nhắc đến sau.

Người dùng gói này có thể nhập từng mô-đun riêng lẻ từ gói, ví dụ:

import Sound.Effects.echo

Nó sẽ nạp mô-đun con Sound.Effects.echo. Nó phải được tham chiếu bằng tên đầy đủ.

Sound.Effects.echo.echofilter(input, output, delay=0.7, atten=4)

Cách nhập mô-đun con khác là:

from Sound.Effects import echo

Nó cũng nạp luôn mô-đun con echo, và làm cho nó có thể được truy cập mà không cần phần tên gói, do đó nó có thể được dùng như sau:

echo.echofilter(input, output, delay=0.7, atten=4)

Một biến thể khác là nhập hàm hoặc biến mình muốn một cách trực tiếp:

from Sound.Effects.echo import echofilter

Một lần nữa, lệnh này nạp mô-đun con echo, nhưng nó làm hàm echofilter() có thể được sử dụng trực tiếp:

echofilter(input, output, delay=0.7, atten=4)

Lưu ý rằng khi sử dụng from package import item, item có thể hoặc là mô-đun con (hoặc gói con) của gói, hoặc là một tên nào khác được định nghĩa trong gói, chẳng hạn như một hàm, lớp, hoặc biến. Câu lệnh import trước hết kiểm tra xem item có được định nghĩa trong gói; nếu không, nó giả định rằng đó là mô-đun và thử nạp nó. Nếu không thể tìm thấy mô-đun, biệt lệ ImportError sẽ được nâng.

Ngược lại, khi dùng cú pháp như import item.subitem.subsubitem, mỗi tiểu mục trừ tiểu mục cuối phải là một gói; tiểu mục cuối có thể là một mô-đun hoặc một gói nhưng không thể là một lớp, hay hàm, hay biến được định nghĩa trong tiểu mục trước.


6.4.1 Nhập * từ một gói

Bây giờ chuyện gì xảy ra khi bạn viết from Sound.Effects import *? Tốt nhất, bạn sẽ hy vọng mã này sẽ tìm các mô-đun có trong gói và nhập tất cả chúng vào. Đáng tiếc là tác vụ này không chạy ổn định lắm trên hệ Mac và Windows vì hệ thống tập tin không luôn chứa thông tin chính xác về phân biệt hoa/thường trong tên tập tin. Trên các hệ này, không có cách nào bảo đảm để biết được nếu một tập tin ECHO.PY cần được nhập vào như là một mô-đun echo, Echo hay ECHO. (Ví dụ, Windows 95 có một thói quen là hiện mọi tên tập tin với ký tự đầu tiên viết hoa.) Hạn chế tên 8+3 của DOS cũng tạo ra thêm một vấn đề thú vị với các tên mô-đun dài.

Giải pháp duy nhất là để tác giả gói chỉ định rõ chỉ mục của gói. Câu lệnh import dùng cách thức sau: nếu mã __init__.py của gói có định nghĩa một danh sách tên __all__, nó sẽ được dùng làm danh sách của các tên mô-đun cần được nhập khi gặp from package import * . Tác giả gói sẽ phải tự cập nhật danh sách này mỗi khi phiên bản mới của gói được phát hành. Tác giả gói cũng có thể không hỗ trợ nó, nếu họ không thấy cách dùng importing * là hữu dụng đối với gói của họ. Ví dụ, tập tin Sounds/Effects/__init__.py có thể chứa đoạn mã sau:

__all__ = ["echo", "surround", "reverse"]

Điều này có nghĩa là from Sound.Effects import * sẽ nhập ba mô-đun được chỉ định từ gói Sound .

Nếu __all__ không được định nghĩa, câu lệnh from Sound.Effects import * does không nhập mọi mô-đun con từ gói Sound.Effects vào vùng tên hiện tại; nó chỉ đảm bảo rằng gói Sound.Effects đã được nhập (có thể chạy các mã khởi tạo trong __init__.py) và sau đó nhập các tên được định nghĩa trong gói. Nó bao gồm các tên (và mô-đun con đã được nạp) được định nghĩa bởi __init__.py. Nó cũng gồm các mô-đun con của gói đã được nạp bởi câu lệnh import trước. Xem đoạn mã này:

import Sound.Effects.echo
import Sound.Effects.surround
from Sound.Effects import *

Trong ví dụ này, các mô-đun echosurround được nhập vào vùng tên hiện tại vì chúng được định nghĩa trong gói Sound.Effects khi câu lệnh from...import được thực thi. (Cũng sẽ có cùng kết quả khi __all__ được định nghĩa.)

Lưu ý là bình thường việc nhập * từ một mô-đun hay gói được coi là xấu, vì nó hay dẫn tới các mã khó đọc. Tuy nhiên, khi dùng trong một phiên làm việc tương tác thì nó giúp tiết kiệm công nhập phím, và một vài mô-đun được thiết kế để chỉ xuất các tên theo một vài mẫu cụ thể.

Nhớ rằng, không có gì sai trái với việc dùng from Package import specific_submodule! Đúng ra, đây là cách viết được khuyến khích trừ khi mô-đun nhập (importing module) cần dùng mô-đun con có cùng tên ở một gói khác.

6.4.2 Tham chiếu nội trong gói

Các mô-đun con thường cần tham chiếu lẫn nhau. Ví dụ, Mô-đun surround có thể sử dụng mô-đun echo . Trong thực tế, những tham chiếu này quá phổ biến đễn nỗi câu lệnh import đầu tiên sẽ tìm trong gói chứa (containing package) trước khi tìm trong đường dẫn tìm kiếm mô-đun chuẩn. Do đó, mô-đun surround có thể đơn giản dùng import echo hay from echo import echofilter. Nếu mô-đun được nhập (imported module) không thể tìm ra trong gói chứa (là gói mà mô-đun hiện tại là một mô-đun con), câu lệnh import tìm một mô-đun cấp cao nhất có cùng tên.

Khi các gói được cấu trúc thành các gói con (như gói Sound trong ví dụ), không có đường tắt để tham chiếu tới các mô-đun con của các gói kế cận - tên đầy đủ của gói con phải được chỉ định. Ví dụ, nếu mô-đun Sound.Filters.vocoder cần dùng mô-đun echo trong gói Sound.Effects , nó có thể dùng from Sound.Effects import echo.

Bắt đầu từ Python 2.5, ngoài việc nhập tương đối hiểu ngầm (implicit relative import) đã nói, bạn có thể viết lệnh nhập tương đối xác định (explicit relative import) với kiểu from module import name của câu lệnh nhập. Các lệnh nhập tương đối xác định dùng chấm ở đầu để chỉ định các gói hiện tại và gói cha có mặt trong câu lệnh nhập. Ví dụ từ mô-đun surround , bạn sẽ dùng:

from . import echo
from .. import Formats
from ..Filters import equalizer

Lưu ý cả hai lệnh nhập tương đối xác định và hiểu ngầm đều dựa vào tên của mô-đun hiện tại. Vì tên của mô-đun chính luôn luôn là "__main__", mô-đun được nhằm dể dùng như mô-đun chính của một chương trình Python nên luôn luôn cùng câu lệnh nhập tuyệt đối.

6.4.3 Gói trong nhiều thư mục

Gói còn có thêm một thuộc tính đặc biệt khác, __path__. Nó được khởi tạo là danh sách chứa tên của thư mục chứa tập tin __init__.py của gói trước khi mã trong tập tin đó được thực thi. Biến này có thể được thay đổi; làm như vậy sẽ ảnh hưởng các tìm kiếm mô-đun và gói con chứa trong gói này trong tương lai.

Mặc dù tính năng này không thường được dùng, nó có thể được tận dụng để mở rộng tập hợp các mô-đun tìm thấy trong một gói.



Ghi chú

... đâu đó.6.1
Thật ra các định nghĩa hàm cũng là `các câu lệnh' được `thực thi'; việc thực thi câu lệnh này nhập tên hàm vào bảng ký hiệu toàn cục của mô-đun.
Xem Về tài liệu này... về cách đề nghị thay đổi.