9.1 Tạo phụ thuộc vòng tròn giữa các module
Giả sử bạn có hai file, a.py
và b.py
, mỗi cái đều
import cái kia như sau:
Trong a.py
:
import b
def f():
return b.x
print(f())
Trong b.py
:
import a
x = 1
def g():
print(a.f())
Đầu tiên thử import a.py
:
import a
# 1
Hoạt động tuyệt vời. Có thể bạn ngạc nhiên. Cuối cùng, các module import vòng tròn lẫn nhau, và điều này có thể sẽ là vấn đề chứ, phải không?
Câu trả lời là chỉ đơn giản có vòng lặp import module không tự nó là một vấn đề trong Python. Nếu module đã được import, Python đủ thông minh để không cố gắng import lại nó. Tuy nhiên, phụ thuộc vào thời điểm mà mỗi module cố gắng truy cập các hàm hoặc biến được định nghĩa trong module khác, bạn thực sự có thể gặp phải vấn đề.
Quay lại ví dụ của chúng ta, khi chúng ta import a.py
, nó không gặp vấn đề gì với việc import b.py
, bởi vì b.py
không yêu cầu bất cứ thứ gì từ a.py
được xác định trong quá trình import. Tham chiếu duy nhất trong b.py
tới a
là lệnh gọi a.f()
. Nhưng lệnh gọi này nằm trong g()
, và không có gì trong a.py
hoặc b.py
gọi g()
. Vì vậy, mọi thứ hoạt động tốt.
Nhưng sẽ ra sao nếu chúng ta thử import b.py
(mà không import a.py
trước đó, có nghĩa là):
import b
Traceback (most recent call last):
File "<stdin>", line 1, in
File "b.py", line 1, in
import a
File "a.py", line 6, in
print(f())
File "a.py", line 4, in f
return b.x
AttributeError: 'module' object has no attribute 'x'
Vấn đề ở đây là trong quá trình import b.py
, nó cố gắng import a.py
, cái mà lượt gọi f()
, đang cố gắng truy cập b.x
. Nhưng b.x
chưa được xác định. Do đó, xảy ra ngoại lệ AttributeError
.
Ít nhất, một trong những cách giải quyết vấn đề này khá đơn giản. Chỉ cần thay đổi b.py
để import a.py
trong g()
:
x = 1
def g():
import a # Điều này sẽ chỉ được đánh giá khi g() được gọi
print(a.f())
Bây giờ, khi chúng ta import, mọi thứ đều ổn:
import b
b.g()
# 1 Được in lần đầu tiên vì module 'a' gọi 'print(f())' ở cuối
# 1 Được in lần thứ hai, cái này là lệnh gọi 'g()' của chúng ta
9.2 Trùng tên với tên module của thư viện chuẩn Python
Một trong những điểm tuyệt vời của Python là nhiều module được cung cấp sẵn. Nhưng kết quả là, nếu bạn không để ý, bạn có thể gặp phải tình huống tên của bạn trùng với tên của một module trong thư viện chuẩn của Python (ví dụ, trong mã của bạn có thể có một module có tên email.py
, và sẽ xung đột với module cùng tên trong thư viện chuẩn).
Điều này có thể dẫn đến những vấn đề nghiêm trọng. Ví dụ, nếu một trong các module cố gắng import phiên bản module từ thư viện chuẩn của Python, và trong dự án của bạn có module cùng tên, nó sẽ bị nhầm lẫn và import module của bạn thay vì module từ thư viện chuẩn.
Vì vậy, bạn nên cẩn thận để không dùng những tên giống như các module của thư viện chuẩn Python. Dễ dàng hơn nhiều để thay đổi tên module trong dự án của bạn hơn là gửi yêu cầu đổi tên module trong thư viện chuẩn và chờ đợi sự chấp thuận.
9.3 Tầm nhìn của ngoại lệ
Xem xét tệp main.py
sau đây:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int("1"))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
Có vẻ như mọi thứ đều đúng, mã sẽ hoạt động, hãy xem điều gì xảy ra khi chúng ta chạy nó:
$ python main.py 1
Traceback (most recent call last):
File "C:\Projects\Python\TinderBolt\main.py", line 19, in <module>
bad()
File "C:\Projects\Python\TinderBolt\main.py", line 17, in bad
print(e)
^
UnboundLocalError: không thể truy cập biến cục bộ 'e' khi nó không được gán giá trị
Chuyện gì đã xảy ra ở đây? Vấn đề là trong Python đối tượng trong khối ngoại lệ không có sẵn bên ngoài khối đó. (Nguyên nhân của điều này là nếu không thì các đối tượng trong khối đó sẽ được giữ trong bộ nhớ cho đến khi bộ thu gom rác chạy và loại bỏ các tham chiếu đến chúng).
Một trong những cách để tránh vấn đề này là lưu trữ tham chiếu đến đối tượng trong khối ngoại lệ bên ngoài khối đó, để nó vẫn có sẵn. Đây là phiên bản của ví dụ trước sử dụng kỹ thuật này, giúp mã hoạt động:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int("1"))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
9.4 Sử dụng sai phương thức __del__
Khi interpreter xoá một đối tượng, nó kiểm tra xem đối tượng đó có hàm __del__
không, và nếu có, nó gọi hàm này trước khi xoá đối tượng. Điều này rất tiện khi bạn muốn đối tượng của mình làm sạch một số tài nguyên bên ngoài hoặc bộ nhớ đệm.
Giả sử bạn có file mod.py
như sau:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
Và bạn cố gắng làm điều này từ file khác another_mod.py
:
import mod
mybar = mod.Bar()
Và bạn nhận được lỗi AttributeError
khủng khiếp.
Tại sao? Bởi vì như đã thông báo ở đây, khi interpreter kết thúc, tất cả các biến toàn cục của module có giá trị None
. Kết quả là trong ví dụ trên, vào thời điểm gọi __del__
, tên foo
đã được thiết lập thành None
.
Giải pháp cho "bài toán có sao" này là sử dụng phương thức đặc biệt atexit.register()
. Như vậy, khi chương trình của bạn kết thúc thực thi (tức là khi thoát ra bình thường), các handle
của bạn sẽ bị xóa trước khi interpreter kết thúc.
Với sửa chữa này, mã mod.py
bên trên có thể trông như thế này:
import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
Cách thực hiện này đảm bảo một cách đơn giản và đáng tin cậy để gọi bất kỳ dọn dẹp cần thiết nào sau khi chương trình kết thúc bình thường. Rõ ràng là quyết định về cách xử lý đối tượng nào đó liên kết với tên self.myhandle
phụ thuộc vào foo.cleanup
, nhưng tôi nghĩ bạn đã hiểu ý tưởng.
GO TO FULL VERSION