Nội dung phần này:
  • (sẽ cập nhật sau)

Danh sách đầy đủ bài học ở đây .

01 Giới thiệu về Descriptors


Đầu tiên, giả sử rằng chúng ta có vấn đề cần giải quyết như sau: chúng ta muốn class Point2D mà giá trị trục số luôn luôn là số nguyên.
Plain attribute cho x và y không thể đảm bảo điều này, thay vào đó chúng ta dùng property với getter, setter.
Chúng ta có đoạn code như ở dưới:
 
class Point2D:
  @property
  def x(self):
    return self._x
  
  @x.setter
    def x(self, value):
      self._x = int(value)
  
  @property
    def y(self):
      return self._y
  
  @y.setter
  def y(self, value):
    self._y = int(value)
  
  def __init__(self, x, y):
    self.x = x
    self.y = y
Đoạn code này khá rườm rà và lặp lại, chúng ta có thể chia thành 2 class như ở dưới:
 
class IntegerValue:
  def get(self):
    return self._value

  def set(self, value):
    self._value = int(value)
  
  def __init__(self, value=None):
    if value:
      self.set(value)

class Point2D:
  x = IntegerValue()
  y = IntegerValue()      
Chúng ta lưu lại với tên OOP08_ex01.py, import và chạy thử, các bạn có thể down file này ở đây.
 
>>> from OOP08_ex01 import * # mình lười gõ, thực tế tuyệt đối không import *
>>> p = Point2D()
>>> p.x = 100.1
>>> p.x
100.1
Chúng ta thấy là kết quả không đúng, lí do là khi chúng ta gọi p.x = 100.1 thì chúng ta tạo một atrribute x mới có kiểu float, hoàn toàn không cột được x với class IntegerValue.
 
>>> type(p)
<class 'OOP08_ex01.Point2D'>
>>> type(p.x)
<class 'float'>
Chúng ta đã thấy ở trên, type(p.x) không trả về 'OOP08_ex01.IntegerValue'.
Chúng ta cần bảo Python làm 2 việc:
  • x = IntegerValue, phải cột biến x vào class này ở run-time.
  • p.x, sử dụng get và set cho instance của class IntegerValue

Đó là lí do Descriptors xuất hiện, chúng ta có 4 method cho descriptor protocol.
  • __get__: để lấy giá trị của attribute, p.x
  • __set__: để set giá trị của attribute, p.x = 100
  • __delete__: để xóa attribute
  • __set_name__: mới xuất hiện ở Python 3.6, dùng để lấy property name

Và các descriptor này được chia thành 2 mục chính:
  • __get__, (__set_name__, optional): non-data descriptor
  • __set__, __delete__: data descriptor
Chúng ta coi ví dụ ở dưới về cách sử dụng __get__:
 
from datetime import datetime

class TimeUTC:
  def __get__(self, instance, owner_class): # chúng ta sẽ giải thích những arg này sau
    return datetime.utcnow().isoformat()

class Logger:
  current_time = TimeUTC()
Chúng ta lưu lại với tên OOP08_ex02.py, import và chạy thử, các bạn có thể down file này ở đây.
 
>>> from OOP08_ex02 import *
>>> 
>>> Logger.__dict__
mappingproxy({'__module__': 'OOP08_ex02', 
'current_time': <OOP08_ex02.TimeUTC object at 0x000001D6EE402C70>, 
'__dict__': <attribute '__dict__' of 'Logger' objects>, 
'__weakref__': <attribute '__weakref__' of 'Logger' objects>, 
'__doc__': None})
>>> 
>>> l = Logger()
>>> l.current_time
'2020-09-07T04:57:55.332443'
Một ví dụ khác, chẳng hạn chúng ta có một bộ bài, và cần rút 10 lá ngẫu nhiên:
 
from random import choice, seed

class Deck:
    @property
    def suit(self):
        return choice(('Spade', 'Heart', 'Diamond', 'Club'))
        
    @property
    def card(self):
        return choice(tuple('23456789JQKA') + ('10',))
Mình giải thích xíu về tiếng Anh: 'Spade': chuồn, 'Heart': cơ, 'Diamond': rô, 'Club': pích và bộ tứ này gọi là Suit. Và 2 tới 10 với J, Q, K, A gọi là card.
Chúng ta lưu lại với tên OOP08_ex03.py, import và chạy thử, các bạn có thể down file này ở đây.
 
>>> from OOP08_ex03 import * 
>>> d = Deck()
>>> seed(0)
>>> for _ in range(10):
...     print(d.card, d.suit)
...
8 Club
2 Diamond
J Club
8 Diamond
9 Diamond
Q Heart
J Heart
6 Heart
10 Spade
Q Diamond
Chúng ta thấy rằng cả 2 property đều làm cùng 1 thứ và chạy song song.
Chúng ta viết lại sử dụng descriptor:
 
class Choice:
    def __init__(self, *choices):
        self.choices = choices
        
    def __get__(self, instance, owner_class):
        return Choice(self.choices)

class Deck:
    suit = Choice('Spade', 'Heart', 'Diamond', 'Club')
    card = Choice(*'23456789JQKA', '10')        

Chúng ta lưu lại với tên OOP08_ex04.py, import và chạy thử, các bạn có thể down file này ở đây.
 
>>> from OOP08_ex04 import * 
>>> d = Deck()
>>> seed(0)
>>> for _ in range(10):
...     print(d.card, d.suit)
... 
8 Club   
2 Diamond
J Club   
8 Diamond
9 Diamond
Q Heart  
J Heart  
6 Heart  
10 Spade 
Q Diamond
Chúng ta thấy rằng kết quả không đổi.

02 Giới thiệu về Getter và Setter