Phân tích cơ chế bộ nhớ và thuộc tính tĩnh trong Python

Cơ chế tham chiếu và biến đổi bộ nhớ

Trong Python, việc hiểu rõ cách các kiểu dữ liệu khác nhau quản lý bộ nhớ là nền tảng quan trọng để tránh các lỗi logic phổ biến. Chúng ta sẽ xem xét sự khác biệt giữa kiểu dữ liệu bất biến (immutable) và kiểu dữ liệu có thể biến đổi (mutable) thông qua việc quan sát địa chỉ bộ nhớ.

1. Kiểu chuỗi (String)

Chuỗi ký tự trong Python là đối tượng bất biến. Khi bạn gán một biến cho một biến khác, cả hai sẽ trỏ về cùng một vùng nhớ. Tuy nhiên, nếu bạn gán lại giá trị mới cho biến gốc, một vùng nhớ mới sẽ được cấp phát, trong khi biến sao chép vẫn giữ nguyên địa chỉ cũ.

text_alpha = 'dữ_liệu_gốc'
text_beta = text_alpha

print(f"Địa chỉ alpha: {id(text_alpha)}")
print(f"Địa chỉ beta: {id(text_beta)}")

print("=== Thay đổi giá trị alpha ===")
text_alpha = 'dữ_liệu_mới'

print(f"Địa chỉ alpha mới: {id(text_alpha)}")
print(f"Địa chỉ beta cũ: {id(text_beta)}")

Kết quả cho thấy khi gán lại text_alpha, địa chỉ bộ nhớ của nó thay đổi hoàn toàn, nhưng text_beta vẫn trỏ đến vùng nhớ chứa 'dữ_liệu_gốc'. Điều này khẳng định việc gán lại không sửa đổi đối tượng cũ mà tạo ra đối tượng mới.

2. Kiểu danh sách (List)

Ngược lại với chuỗi, danh sách là đối tượng có thể biến đổi. Khi thực hiện các thao tác như thêm phần tử, địa chỉ bộ nhớ của đối tượng danh sách không thay đổi. Các biến tham chiếu đến danh sách đó sẽ cùng phản ánh sự thay đổi.

buffer_a = [10, 20, 30]
buffer_b = buffer_a

print(f"ID buffer_a: {id(buffer_a)}")
print(f"ID buffer_b: {id(buffer_b)}")

print("=== Thêm phần tử vào buffer_a ===")
buffer_a.append(40)

print(f"ID buffer_a sau sửa: {id(buffer_a)}")
print(f"ID buffer_b sau sửa: {id(buffer_b)}")
print(f"Nội dung buffer_b: {buffer_b}")

Do buffer_abuffer_b cùng trỏ đến một vùng nhớ chứa danh sách, việc thêm phần tử vào buffer_a cũng làm thay đổi nội dung khi truy cập qua buffer_b. Địa chỉ bộ nhớ giữ nguyên vì đối tượng danh sách không bị tái khởi tạo.

3. Minh họa với Class tùy chỉnh

Nguyên lý này cũng áp dụng cho các đối tượng do người dùng định nghĩa. Nếu thay đổi thuộc tính bên trong đối tượng, tham chiếu không đổi. Nếu gán lại biến đó bằng một đối tượng mới, tham chiếu sẽ đổi.

class Container:
    def __init__(self, content):
        self.content = content
    
    def update_content(self, suffix):
        self.content += suffix

obj_x = Container('giá_trị_đầu')
obj_y = obj_x

print(f"Nội dung X: {obj_x.content}")
print(f"Nội dung Y: {obj_y.content}")
print(f"ID X: {id(obj_x)}")

obj_x.update_content('_đã_sửa')

print("=== Sau khi cập nhật nội dung ===")
print(f"Nội dung X: {obj_x.content}")
print(f"Nội dung Y: {obj_y.content}")
print(f"ID X: {id(obj_x)}")

# Gán lại đối tượng mới
obj_x = Container('đối_tượng_mới')

print("=== Sau khi gán lại đối tượng mới ===")
print(f"Nội dung X: {obj_x.content}")
print(f"Nội dung Y: {obj_y.content}")
print(f"ID X: {id(obj_x)}")
print(f"ID Y: {id(obj_y)}")

Thuộc tính lớp (Static Fields) và thuộc tính đối tượng

Trong lập trình hướng đối tượng Python, cần phân biệt rõ giữa thuộc tính lớp (shared across all instances) và thuộc tính đối tượng (unique per instance). Việc truy cập và sửa đổi sai cách có thể dẫn đến hành vi không như mong đợi.

1. Cơ chế truy cập và lưu trữ

Thuộc tính lớp được định nghĩa trực tiếp trong class và thường được truy cập qua tên lớp. Thuộc tính đối tượng được định nghĩa trong __init__ và truy cập qua self. Khi truy cập thuộc tính lớp qua đối tượng, Python sẽ tìm kiếm trong __dict__ của đối tượng trước, nếu không thấy sẽ tìm lên lớp.

class BankAccount:
    interest_rate = 0.05  # Thuộc tính lớp
    
    def __init__(self, owner):
        self.owner = owner  # Thuộc tính đối tượng

acc_1 = BankAccount('Nguyen Van A')
acc_2 = BankAccount('Tran Thi B')

print(f"Lãi suất chung: {BankAccount.interest_rate}")
print(f"Chủ tài khoản 1: {acc_1.owner}")

# Kiểm tra namespace
print(f"__dict__ của acc_1: {acc_1.__dict__}")
print(f"Khóa trong Class: {list(BankAccount.__dict__.keys())}")

2. Cảnh báo khi sửa đổi thuộc tính lớp qua đối tượng

Một lỗi phổ biến là cố gắng thay đổi giá trị thuộc tính lớp thông qua biến đối tượng. Thao tác này không sửa đổi giá trị chung của lớp mà sẽ tạo ra một thuộc tính mới trùng tên trong namespace của riêng đối tượng đó (shadowing).

class BankAccount:
    interest_rate = 0.05
    
    def __init__(self, owner):
        self.owner = owner

acc_1 = BankAccount('User A')
acc_2 = BankAccount('User B')

# Sai lầm: Gán qua đối tượng
acc_1.interest_rate = 0.10
acc_2.interest_rate = 0.15

print(f"Lãi suất acc_1: {acc_1.interest_rate}")
print(f"Lãi suất acc_2: {acc_2.interest_rate}")
print(f"Lãi suất gốc Class: {BankAccount.interest_rate}")

print(f"__dict__ acc_1: {acc_1.__dict__}")
print(f"__dict__ acc_2: {acc_2.__dict__}")

Kết quả cho thấy interest_rate trong acc_1acc_2 đã trở thành thuộc tính đối tượng độc lập. Giá trị gốc trong BankAccount vẫn không đổi. Để sửa đổi giá trị chung, bắt buộc phải dùng BankAccount.interest_rate = new_value.

3. Xử lý trùng tên thuộc tính

Nếu một lớp có cả thuộc tính lớp và thuộc tính đối tượng cùng tên, việc truy cập qua đối tượng sẽ ưu tiên thuộc tính đối tượng. Để truy cập thuộc tính lớp trong trường hợp này, cần dùng tham chiếu đến class thông qua đối tượng.

class BankAccount:
    limit = 5000
    
    def __init__(self, owner, limit):
        self.owner = owner
        self.limit = limit  # Che khuất thuộc tính lớp

acc = BankAccount('User C', 1000)

print(f"Giới hạn qua Class: {BankAccount.limit}")
print(f"Giới hạn qua Object: {acc.limit}")
print(f"Truy cập Class qua Object: {acc.__class__.limit}")

Kế thừa và thuộc tính tĩnh

Khi làm việc với các lớp con, việc sửa đổi thuộc tính tĩnh cũng tuân theo quy tắc tương tự. Nếu lớp con gán giá trị cho thuộc tính tĩnh được kế thừa, nó sẽ tạo ra một bản sao thuộc tính riêng cho lớp con đó thay vì sửa đổi lớp cha.

class BaseAccount:
    fee = 100

class SavingsAccount(BaseAccount):
    pass

class BusinessAccount(BaseAccount):
    pass

# Sửa đổi qua lớp con
SavingsAccount.fee = 50
BusinessAccount.fee = 200

print(f"Phí gốc (Base): {BaseAccount.fee}")
print(f"Phí Savings: {SavingsAccount.fee}")
print(f"Phí Business: {BusinessAccount.fee}")

print(f"Thành phần Savings: {list(SavingsAccount.__dict__.keys())}")
print(f"Thành phần Business: {list(BusinessAccount.__dict__.keys())}")

Như kết quả hiển thị, BaseAccount.fee vẫn giữ nguyên giá trị ban đầu. SavingsAccountBusinessAccount đã tạo ra thuộc tính fee riêng trong namespace của chúng. Nếu mục đích là thay đổi giá trị chung cho tất cả các loại tài khoản, cần phải gán trực tiếp vào BaseAccount.fee.

Thẻ: python memory-management object-oriented-programming class-attributes

Đăng vào ngày 16 tháng 05 lúc 19:24