+3

Python descriptor

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@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

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí