Python descriptor
Bài đăng này đã không được cập nhật trong 7 năm
Bài viết này mình sẽ giới thiệu một kỹ thuật nâng cao trong descriptor trong Python
1. Ví dụ về descriptor
Xét ví dụ khi chúng ta muốn xây dựng mô hình cho bài toán về các lập trình viên
class Programmer(object):
def __init__(self, name, age, salary, rating):
self.name = name
self.age = age
self.salary = salary
self.rating = rating
Giờ nếu bạn muốn thêm một điều kiện là tuổi của lập trình viên phải luôn lớn hơn 0, bạn có thể cài đặt như sau
class Programmer(object):
def __init__(self, name, age, salary, rating):
self.name = name
self.salary = salary
self.rating = rating
if age > 0:
self.age = age
else:
raise ValueError("Negative value not allowed: %s" % age)
Tuy nhiên với cách làm này, bạn vẫn có thể làm cho age < 0, nếu gán giá trị của age trực tiếp từ instance của Programmer
>>> tienpm = Programmer('tienpm', 26, 500, 5)
>>> tienpm.age = -10
May mắn thay, ta có thể sử dụng property để giải quyết vấn đề này
class Programmer(object):
def __init__(self, name, age, salary, rating):
self._age = None # tạo một thuộc tính private cho age
self.name = name
self.age = age
self.salary = salary
self.rating = rating
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if age > 0:
self._age = value
else:
raise ValueError("Negative value not allowed: %s" % age)
>>> tienpm = Programmer('tienpm', 26, 500, 5)
>>> try:
tienpm.age = -10
except ValueError:
print "Cannot set negative value"
Cannot set negative value
Tạo một biến private cho age. Và sử dụng @getter và @setter để bind thuộc tính age với 2 method. Trong 2 method này, chúng ta sẽ cài đặt logic cho việc gán trị của age. Khi chúng ta gọi tienpm.age = value, python sẽ tự động gọi đến setter của age, còn nếu chỉ gọi tienpm.age (không có gán giá trị), thì getter sẽ được gọi.
2. Vấn đề của getter và setter
Nếu giờ, chúng ta cũng muốn kiểm tra giá trị của hai thuộc tính salary và rating. Chúng ta có thể làm tương tự như sau
class Programmer(object):
def __init__(self, name, age, salary, rating):
self._age = None # tạo một thuộc tính private cho age
self._salary = None # tạo một thuộc tính private cho salary
self._rating = None # tạo một thuộc tính private cho rating
self.name = name
self.age = age
self.salary = salary
self.rating = rating
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if age > 0:
self._age = value
else:
raise ValueError("Negative value not allowed: %s" % age)
@property
def salary(self):
return self._salary
@age.setter
def salary(self, value):
if salary > 0:
self._salary = value
else:
raise ValueError("Negative value not allowed: %s" % age)
@property
def rating(self):
return self._rating
@age.setter
def rating(self, value):
if rating > 0:
self._rating = value
else:
raise ValueError("Negative value not allowed: %s" % age)
Tuy nhiên cách làm này làm cho code của chúng ta có qúa nhiều đoạn code lặp về logic. Đây chính là lúc descriptor có thể sử dụng.
3. Descriptor
Descriptor cho phép chúng ta bind cách xử lý truy cập của một thuộc tính trong class A với một class B khác. Nói cách khác, nó cho phép đưa việc truy cập thuộc tính ra ngoài class. Sau đây là cách cài đặt đối với bài toán của chúng ta
class NonNegativeDescriptor(object):
def __init__(self, label):
self.label = label
def __get__(self, instance, owner):
return instance.__dict__.get(self.label)
def __set__(self, instance, value):
if value > 0:
instance.__dict__[self.label] = value
else:
raise ValueError("Negative value not allowed: %s" % age)
class Programmer(object):
age = NonNegativeDescriptor('age')
salary = NonNegativeDescriptor('salary')
rating = NonNegativeDescriptor('rating')
def __init__(self, name, age, salary, rating):
self.name = name
self.age = age
self.salary = salary
self.rating = rating
>>> tienpm = Programmer('tienpm', 26, 500, 5)
>>> print tienpm.age
>>> tienpm.age = 20
NonNegativeDescriptor là một descriptor vì class này cài đặt 2 phương thức get và set. Python nhận ra một class là descriptor nếu như class đó implement một trong 3 phương thức.
- get: Nhận 2 tham số instance và owner. instance là instance của class mà Descriptor được bind tới. owner là class của instance. Trong trường hợp, không có instance nào được gọi, owner sẽ là None.
- set: Nhận 2 tham số instance và value. instance có ý nghĩa như trong get, value là giá trị muốn set cho thuộc tính của instance
- delete: Nhận 1 tham số instance Trong class Programmer, chúng ta tạo ra 3 Descriptor ở mức class là age, salary và rating. Khi gọi print tienpm.age, python sẽ nhận ra age là một descriptor, nên nó sẽ gọi đến hàm get của descriptor NonNegativeDescriptor.get(tienpm, Programmer). Tương tự khi gán giá trị cho tienpm.age = 20, hàm set của descriptor cũng được gọi NonNegativeDescriptor.set(tienpm, 20).
Nếu chúng ta gọi Programmer.age, thì hàm get sẽ được gọi với owner = None.
Kết luận
Bài viết này giới thiệu với các bạn về descriptor trong Python. Với descriptor, chúng ta có thể chuyển việc can thiệp vào từng thuộc tính của một instance trong class tới việc can thiệp vào thuộc tính ở mức class. Cùng với metaclass, descriptor được sử dụng như một ma thuật đen (black magic) trong metaprogramming. Descriptor được sử dụng rất nhiều khi xây dựng các bộ thư viện về ORM (django ORM, peewee, redisco)
All rights reserved