[Mẹo] Tăng cường khả năng sử dụng từ điển bằng DotDict trong Python
By JoeVu, at: 14:24 Ngày 01 tháng 5 năm 2024
Thời gian đọc ước tính: __READING_TIME__ phút
Khi làm việc với từ điển trong Python, việc truy cập các khóa đôi khi có vẻ rườm rà với ký hiệu dấu ngoặc tiêu chuẩn. Sẽ thật tuyệt nếu có thể truy cập các khóa từ điển dưới dạng các thuộc tính? Trong bài đăng này, chúng ta sẽ chỉ cho bạn cách tạo một lớp DotDict cho phép bạn làm điều đó.
Đây là một bài đăng trên blog về lớp DotDict trong Python: https://glinteco.com/en/post/tips-python-dotdict-class/
Triển khai Lớp DotDict
Lớp DotDict là một lớp con của lớp dict tích hợp sẵn của Python. Bằng cách ghi đè các phương thức __getattr__, __setattr__ và __delattr__, chúng ta cho phép truy cập kiểu thuộc tính vào các khóa từ điển.
class DotDict(dict):
"""Lớp DotDict cho phép truy cập các khóa từ điển dưới dạng thuộc tính."""
def __getattr__(self, attr):
if attr in self:
return self[attr]
raise AttributeError(f"'{self.__class__.__name__}' đối tượng không có thuộc tính '{attr}'")
def __setattr__(self, key, value):
self[key] = value
def __delattr__(self, item):
try:
del self[item]
except KeyError:
raise AttributeError(f"'{self.__class__.__name__}' đối tượng không có thuộc tính '{item}'")
# Ví dụ sử dụng:
my_dict = {'name': 'Joe', 'age': 30, 'city': 'Hanoi'}
dot_dict = DotDict(my_dict)
# Truy cập các khóa từ điển dưới dạng thuộc tính
print(dot_dict.name) # Đầu ra: Joe
print(dot_dict.age) # Đầu ra: 30
print(dot_dict.city) # Đầu ra: Hanoi
# Sửa đổi các giá trị bằng cách sử dụng các thuộc tính
dot_dict.age = 31
print(dot_dict.age) # Đầu ra: 31
# Xóa một thuộc tính (cặp khóa-giá trị)
del dot_dict.city
# Truy cập một thuộc tính đã xóa sẽ gây ra lỗi AttributeError
# print(dot_dict.city) # Việc bỏ nhận xét dòng này sẽ gây ra lỗi AttributeError
Cách nó hoạt động
__getattr__: Cho phép bạn truy cập các khóa từ điển dưới dạng các thuộc tính. Nếu khóa không tồn tại, nó sẽ gây ra lỗiAttributeError.
__setattr__: Cho phép bạn đặt các khóa từ điển bằng cách sử dụng phép gán kiểu thuộc tính.
__delattr__: Cho phép xóa các khóa từ điển bằng từ khóadel.
Cải tiến
1. Xử lý các Khóa bị thiếu một cách duyên dáng:
Truy cập một khóa bị thiếu bằng cách sử dụng ký hiệu dấu chấm sẽ gây ra KeyError. Bạn có thể thêm một phương thức __getattr__ xử lý các khóa bị thiếu một cách duyên dáng, có khả năng trả về None hoặc gây ra AttributeError để giữ cho nó nhất quán với việc truy cập thuộc tính thông thường.
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(f"'DotDict' đối tượng không có thuộc tính '{name}'")
2. Xử lý Mặc định Khởi tạo:
__init__ giả định rằng một từ điển luôn được truyền vào. Để làm cho nó linh hoạt hơn, hãy cân nhắc cho phép một từ điển trống theo mặc định:
def __init__(self, dct=None):
dct = dct or {}
for key, value in dct.items():
self[key] = DotDict(value) if isinstance(value, dict) else value
3. Xử lý Đệ quy:
Bạn đã xử lý tốt các từ điển lồng nhau. Tuy nhiên, hãy đảm bảo rằng nó không vô tình lặp lại vô hạn nếu nó gặp phải tham chiếu tự thân.
my_dict = {'name': 'Joe', 'age': 30, 'address': {'city': 'hanoi'}}
dot_dict = DotDict(my_dict)
dot_dict.address.city
---------------------------------------------------------------------------
AttributeError Traceback (lần gọi cuối nhất)
Cell In[6], line 1
----> 1 dot_dict.address.city
AttributeError: 'dict' object has no attribute 'city'
Một phiên bản tốt hơn
class DotDict(dict):
"""Lớp DotDict cho phép truy cập các khóa từ điển dưới dạng thuộc tính."""
def __getattr__(self, name):
try:
return self[name]
except KeyError:
raise AttributeError(f"'DotDict' object has no attribute '{name}'")
def __setattr__(self, name, value):
self[name] = value
def __delattr__(self, name):
try:
del self[name]
except KeyError:
raise AttributeError(f"'DotDict' object has no attribute '{name}'")
def __init__(self, dct=None):
dct = dct or {}
for key, value in dct.items():
if not isinstance(key, str) or not key.isidentifier():
raise ValueError(f"Khóa không hợp lệ cho ký hiệu dấu chấm: {key}")
self[key] = DotDict(value) if isinstance(value, dict) else value
def __repr__(self):
return f"DotDict({super().__repr__()})"
# Ví dụ sử dụng
my_dict = my_dict = {'name': 'Joe', 'age': 30, 'address': {'city': 'hanoi'}}
dot_dict = DotDict(my_dict)
print(dot_dict.address.city) # Đầu ra: hanoi
Kiểm tra Lớp DotDict
Để đảm bảo lớp DotDict của chúng ta hoạt động chính xác, chúng ta có thể viết một số bài kiểm tra bằng cách sử dụng pytest. Đây là một bộ kiểm tra bao gồm các trường hợp sử dụng khác nhau:
import pytest
from dotdict import DotDict # Giả sử lớp DotDict nằm trong một tệp có tên dotdict.py
@pytest.fixture
def sample_data():
"""Fixture cho dữ liệu mẫu."""
return {
"company": "Glinteco",
"founder": "Joe",
"slogan": "Đội Python Tốt nhất",
"address": {
"street": "302 Cầu Giấy",
"city": "Hà Nội",
"country": "Việt Nam",
},
"teams": ["Đội Python Hà Nội", "Đội Python Việt Nam"],
"established_year": 2024,
"next_goals_year": 2025,
}
def test_dotdict_initialization(sample_data):
"""Kiểm tra khởi tạo của DotDict."""
data = DotDict(sample_data)
assert data.company == "Glinteco"
assert data.founder == "Joe"
assert data.slogan == "Đội Python Tốt nhất"
assert data.address.street == "302 Cầu Giấy"
assert data.address.city == "Hà Nội"
assert data.address.country == "Việt Nam"
assert data.teams == ["Đội Python Hà Nội", "Đội Python Việt Nam"]
assert data.established_year == 2024
assert data.next_goals_year == 2025
def test_dotdict_attribute_assignment(sample_data):
"""Kiểm tra việc đặt thuộc tính trong DotDict."""
data = DotDict(sample_data)
data.ceo = "Joe Nguyễn"
assert data.ceo == "Joe Nguyễn"
data.address.city = "Hồ Chí Minh"
assert data.address.city == "Hồ Chí Minh"
def test_dotdict_nested_update(sample_data):
"""Kiểm tra cập nhật từ điển lồng nhau."""
data = DotDict(sample_data)
data.address["street"] = "123 Trần Phú"
assert data.address.street == "123 Trần Phú"
def test_dotdict_key_error(sample_data):
"""Kiểm tra việc truy cập các thuộc tính bị thiếu sẽ gây ra AttributeError."""
data = DotDict(sample_data)
with pytest.raises(AttributeError):
_ = data.non_existent_key
def test_dotdict_delattr(sample_data):
"""Kiểm tra việc xóa các thuộc tính."""
data = DotDict(sample_data)
del data.address
with pytest.raises(AttributeError):
_ = data.address
def test_dotdict_list_access(sample_data):
"""Kiểm tra truy cập danh sách trong DotDict."""
data = DotDict(sample_data)
assert data.teams[0] == "Đội Python Hà Nội"
assert data.teams[1] == "Đội Python Việt Nam"
def test_dotdict_recursive_initialization(sample_data):
"""Kiểm tra khởi tạo đệ quy của DotDict lồng nhau."""
data = DotDict(sample_data)
assert isinstance(data.address, DotDict)
assert isinstance(data.address.city, str)
assert data.address.city == "Hà Nội"
def test_dotdict_invalid_key():
"""Kiểm tra DotDict sẽ gây ra ValueError đối với các khóa không hợp lệ."""
with pytest.raises(ValueError):
DotDict({"1invalid-key": "test"})
if __name__ == "__main__":
pytest.main()
Bạn có thể chạy các bài kiểm tra này bằng cách điều hướng đến thư mục chứa test_dotdict.py và thực thi:
pytest test_dotdict.py
Các bài kiểm tra khác được triển khai bằng unittest:
import unittest
from dotdict import DotDict # Giả sử lớp DotDict nằm trong một tệp có tên dotdict.py
class TestDotDict(unittest.TestCase):
def setUp(self):
"""Thiết lập dữ liệu mẫu cho các bài kiểm tra."""
self.sample_data = {
"company": "Glinteco",
"founder": "Joe",
"slogan": "Đội Python Tốt nhất",
"address": {
"street": "302 Cầu Giấy",
"city": "Hà Nội",
"country": "Việt Nam",
},
"teams": ["Đội Python Hà Nội", "Đội Python Việt Nam"],
"established_year": 2024,
"next_goals_year": 2025,
}
self.data = DotDict(self.sample_data)
def test_initialization(self):
"""Kiểm tra khởi tạo của DotDict."""
self.assertEqual(self.data.company, "Glinteco")
self.assertEqual(self.data.founder, "Joe")
self.assertEqual(self.data.slogan, "Đội Python Tốt nhất")
self.assertEqual(self.data.address.street, "302 Cầu Giấy")
self.assertEqual(self.data.address.city, "Hà Nội")
self.assertEqual(self.data.address.country, "Việt Nam")
self.assertEqual(self.data.teams, ["Đội Python Hà Nội", "Đội Python Việt Nam"])
self.assertEqual(self.data.established_year, 2024)
self.assertEqual(self.data.next_goals_year, 2025)
def test_attribute_assignment(self):
"""Kiểm tra việc đặt thuộc tính trong DotDict."""
self.data.ceo = "Joe Nguyễn"
self.assertEqual(self.data.ceo, "Joe Nguyễn")
self.data.address.city = "Hồ Chí Minh"
self.assertEqual(self.data.address.city, "Hồ Chí Minh")
def test_nested_update(self):
"""Kiểm tra cập nhật từ điển lồng nhau."""
self.data.address["street"] = "123 Trần Phú"
self.assertEqual(self.data.address.street, "123 Trần Phú")
def test_key_error(self):
"""Kiểm tra việc truy cập các thuộc tính bị thiếu sẽ gây ra lỗi AttributeError."""
with self.assertRaises(AttributeError):
_ = self.data.non_existent_key
def test_delattr(self):
"""Kiểm tra việc xóa các thuộc tính."""
del self.data.address
with self.assertRaises(AttributeError):
_ = self.data.address
def test_list_access(self):
"""Kiểm tra truy cập danh sách trong DotDict."""
self.assertEqual(self.data.teams[0], "Đội Python Hà Nội")
self.assertEqual(self.data.teams[1], "Đội Python Việt Nam")
def test_recursive_initialization(self):
"""Kiểm tra khởi tạo đệ quy của DotDict lồng nhau."""
self.assertIsInstance(self.data.address, DotDict)
self.assertIsInstance(self.data.address.city, str)
self.assertEqual(self.data.address.city, "Hà Nội")
def test_invalid_key(self):
"""Kiểm tra DotDict sẽ gây ra ValueError đối với các khóa không hợp lệ."""
with self.assertRaises(ValueError):
DotDict({"1invalid-key": "test"})
if __name__ == "__main__":
unittest.main()
Để thực thi bài kiểm tra
python -m unittest test_dotdict.py
Chúng sẽ chạy các framework unittest/pytest, thực thi các bài kiểm tra và cung cấp phản hồi về sự thành công hoặc thất bại của chúng.
Kết luận
Lớp DotDict là một giải pháp đơn giản và thanh lịch để truy cập các khóa từ điển dưới dạng các thuộc tính. Nó có thể làm cho mã của bạn gọn gàng và trực quan hơn. Tuy nhiên, hãy lưu ý đến những hạn chế của nó và xem xét liệu nó có phù hợp với trường hợp sử dụng của bạn hay không. Nếu bạn gặp các khóa không phải là tên thuộc tính hợp lệ, bạn có thể cần gắn bó với các phương pháp truy cập từ điển truyền thống hoặc sử dụng một cách tiếp cận khác.