Cảm ơn bạn đã đọc và ủng hộ blog KTMT ʘ‿ʘ Từ bây giờ chúng tôi sẽ là kipalog.com !

Giới Thiệu về Python Descriptor

Trong các bài viết trước, chúng tôi đã giới thiệu về các kiến thức cơ bản trong python, như object trong python, decorators.

Bài viết này sẽ giới thiệu một kỹ nâng cao trong Python, đó là descriptor

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

programmer.py
1
2
3
4
5
6
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

programmer.py
1
2
3
4
5
6
7
8
9
10
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

1
2
>>> kiennt = Programmer('kiennt', 26, 500, 5)
>>> kiennt.age = -10

May mắn thay, ta có thể sử dụng property để giải quyết vấn đề này

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)
python
1
2
3
4
5
6
>>> kiennt = Programmer('kiennt', 26, 500, 5)
>>> try:
        kiennt.age = -10
    except ValueError:
        print "Cannot set negative value"
Cannot set negative value

Cách chúng ta làm ở đây đó là tạo ra một biến private _age để chứa giá trị thật của 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 kiennt.age = value, python sẽ tự động gọi đến setter của age, còn nếu chỉ gọi kiennt.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 salaryrating. Chúng ta có thể làm tương tự như sau

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
python
1
2
3
>>> kiennt = Programmer('kiennt', 26, 500, 5)
>>> print kiennt.age
>>> kiennt.age = 20

NonNegativeDescriptor là một descriptor vì class này cài đặt 2 phương thức __get____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ố instanceowner. 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ố instancevalue. 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, salaryrating. Khi gọi print kiennt.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__(kiennt, Programmer). Tương tự khi gán giá trị cho kiennt.age = 20, hàm __set__ của descriptor cũng được gọi NonNegativeDescriptor.__set__(kiennt, 20).

Nếu chúng ta gọi Programmer.age, thì hàm __get__ sẽ được gọi với owner = None.

4. Descriptor và Metaclass

Một điểm cần lưu ý đó là trong descriptor, có sử dụng biến label để bind giữa descriptor và thuộc tính của class. Ta có thể sử dụng Metaclass để giải quyết vấn đề này

programmer.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class NonNegativeDescriptor(object):
    def __init__(self, label=None):
        self.label = label

    def __get__(self, instance, owner):
        return instance.__dict__.get(self.label)

    def __set__(self, instance, value):
        instance.__dict__[self.label] = value

class DescriptorMeta(type):
    def __new__(cls, name, bases, attrs):
        for k, v in attrs.items():
            if isinstance(v, NonNegativeDescriptor):
                v.label = k
        return super(DescriptorMeta, cls).__new__(cls, name, bases, attrs)

class Programmer(object):
    age = NonNegativeDescriptor()
    salary = NonNegativeDescriptor()
    rating = NonNegativeDescriptor()

    def __init__(self, name, age, salary, rating):
        self.name = name
        self.age = age
        self.salary = salary
        self.rating = rating

Metaclass hoạt động như thế nào, sẽ được giới thiệu trong bài viết tiếp theo.

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)

Tham khảo

Comments

Copyright © 2015 kỹ thuật máy tính