Nội dung phần này:

  • Delegate to parent
  • Slots
  • Slots and Single Inheritance

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

05. Delegate to parent

Delegate to parent đại khái như là giao phó cho class cha. Ở phần 06, chúng ta đã thấy một đoạn code khá dài dòng:

class Account:
    apr = 3.0
    
    def __init__(self, account_number, balance):
        self.account_number = account_number
        self.balance = balance
        self.account_type = 'Generic Account'
        
    def calc_interest(self):
        return f'Calc interest on {self.account_type} with APR = {self.apr}'
        
class Savings(Account):
    apr = 5.0
    
    def __init__(self, account_number, balance):
        self.account_number = account_number  # Tạm thời chỗ này hơi dài dòng, sẽ giới thiệu sau.
        self.balance = balance
        self.account_type = 'Savings Account'

Rõ ràng là, __init__ được override ở class Savings là class con của Account, nhưng code của nó thì gần như y hệt. Hoặc như ở ví dụ dưới:

class Person:
	def __init__(self, name, age):
		self.name = name
		self.age = age

class Student(Person):
	def __init__(self, name, age, major):
		self.major = major
		self.name = name
		self.age = age

Chúng ta cũng thấy rằng, nameage được lặp lại, chúng ta có thể gọi thẳng method từ class cha, dùng super(): super().method(). Ví dụ:

class Person:
	def sing(self):
		return "I'm a lumberjack and I'm OK"
		
		
class Student(Person):
	def sing(self):
		return super().sing() + '\n' + "I sleep all night and I work all day" 
		

s = Student()
print(s.sing()) 

I'm a lumberjack and I'm OK
I sleep all night and I work all day

Lưu ý: nhớ phải dùng super().sing(), nếu quên lỡ tay xài self.sing() sẽ bị vòng lặp vô hạn.
Giả sử chúng ta có 3 class: class ông Person có sing(), class con của Person là class cha Student không có sing(), và class con của Student là class cháu MusicStudent lại có sing().
Câu hỏi: khi sing() ở class cháu MusicStudent gọi super().sing() thì nó có gọi được sing() ở class ông Person hay không?

class Person:
	def sing(self):
		return "I'm a lumberjack and I'm OK"

class Student(Person):
	pass
	
class MusicStudent(Student):
	def sing(self):
		return super().sing() + '\n' + "I sleep all night and I work all day" 

Điều này đương nhiên là được, lí do cũng khá dễ hiểu, Student thừa kế lại Person nên bản thân class Student không có code gì nhưng thực sự có thừa kế sing() từ Person, khi gọi super().sing() ở MusicStudent thì nó sẽ gọi sing() ở Student và sing() ở Student có tồn tại.

Cái ví dụ _init__ ở trên kia thì nên viết lại như ở dưới:

class Person:
	def __init__(self, name, age):
		self.name = name
		self.age = age

class Student:
	def __init__(self, name, age, major):
		super().__init__(name, age)
		self.major = major

Khi chúng ta giao phó cho class cha, chúng ta không bắt buộc nhưng nên gọi super()__init__ TRƯỚC khi gán major.
Chúng ta xem ví dụ ở dưới:

>>> class Person:
...     def __init__(self, name, age):
...             self.name = name
...             self.age = age
...             self.major = 'N/A'
...
>>> class Student(Person):
...     def __init__(self, name, age, major):
...             self.major = major
...             super().__init__(name, age)
...
>>>
>>> s = Student('douglas', 42, 'literature')
>>> s.name
'douglas'
>>> s.age
42
>>> s.major
'N/A'

Chúng ta thấy rằng s.major'N/A' chứ không phải là 'literature', lí do là: ban đầu ‘literature’ đã được gán vào s.major rồi, nhưng sao đó gọi super().__init__ thì __init__ sẽ chạy lại toàn bộ gán ở class Person, do đó ‘N/A’ lại được gán một lần nữa vào s.major.
Đó là lí do chúng ta nên gọi super().__init__ trước tiên, để có gán gì mới thì có thể override các giá trị đã được gán ở class cha.

Như ở phần 06, phần Extending, chúng ta sẽ có câu hỏi: self thuộc class nào?.
Cách để nhớ là khi sử dụng super(), object là instance của class nào thì self sẽ bị cột vào class đó.
Chúng ta xem ví dụ:

>>> class Person:
...     def hello(self):
...             print('In Person class:', self)
...
>>> class Student(Person):
...     def hello(self):
...             print('In Student class:', self)
...             super().hello()
...
>>> p = Person()
>>> s = Student()
>>>
>>> p.hello()
In Person class: <__main__.Person object at 0x0000015C863E7400>
>>> s.hello()
In Student class: <__main__.Student object at 0x0000015C863E78E0>
In Person class: <__main__.Student object at 0x0000015C863E78E0>

Chúng ta gọi s.hello() trước tiên sẽ chạy lên print, in ra cho biết đang ở Student class, sau đó sẽ chạy tiếp super().hello(), vì có super() nên sẽ chạy method hello() trên class Person, tuy nhiên self ở đây dù ở Person class nhưng bản thân nó vẫn là Student class In Person class: <__main__.Student object … >.

06. Slots

Ở phần Class, chúng ta đã biết rằng các attribute của một instance thì sẽ được lưu ở một local dictionary:

>>> class Point:
...     def __init__(self, x, y):
...             self.x = x
...             self.y = y
...
>>> p = Point(0, 0)
>>> p.__dict__
{'x': 0, 'y': 0}

Nếu có quá nhiều instace thì dictionary sẽ bị overhead -> __slots__ được sinh ra, để thu gọn (compact) data structure.

>>> class Point:
...     __slots__ = ('x', 'y')
...     def __init__(self, x, y):
...             self.x = x
...             self.y = y
...
>>> p = Point(0, 0)
>>> p.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute '__dict__'
>>> vars(p)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: vars() argument must have __dict__ attribute
>>> dir(p)
[..., 'x', 'y']
>>> p.x = 100
>>> p.x
100

Với class Point như trên, nếu không dùng __slots__ thì 10000 instance sẽ tốn khoảng 1729KB, còn sử dụng __slots__ thì tốn khoảng 635KB.
Thời gian truy xuất cũng nhanh hơn:

>>> class Person:
...     pass
...
>>> def check_dict():
...     p = Person()
...     p.name = 'Dat'
...     p.name
...     del p.name
...
>>> from timeit import timeit
>>> timeit(check_dict)
0.25444989999959944
>>> from timeit import timeit
>>> timeit(check_dict)
0.25444989999959944
>>> class PersonSlots:
...     __slots__ = ('name', )
...
>>>
>>> def check_slots():
...     p = PersonSlots()
...     p.name = 'Dat'
...     p.name
...     del p.name
...
>>> from timeit import timeit
>>> timeit(check_slots)
0.18494569999984378

Chúng ta thấy nó nhanh hơn khoảng 30%.

Tuy nhiên không phải lúc nào cũng dùng __slots__ vì nó không cho gán attribute mới, sẽ gặp khó khăn trong việc đa thừa kế:

>>> p.z = 0
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'

07. Slots and Single Inheritance

Chuyện gì xảy ra nếu chúng ta tạo một class cha có __slots__ và class con thừa kế class cha này mà không có __slots__:

>>> class Person:
...     __slots__ = ('name', )
...
>>> class Student(Person):
...     pass
...
>>> s = Student()
>>> s.name = 'Dat'
>>> s.__dict__
{}
>>> s.age = 35
>>> s.__dict__
{'age': 35}

Chúng ta thấy rằng, __slots__ không gây trở ngại gì cho đơn thừa kê, name được khai báo trong __slots__ nên nó không có tồn tại trong dict, nhưng gán attribute mới là age thì vẫn không báo lỗi gì, và attribute này được cập nhật vào dict.
Chúng ta xem thêm ví dụ dưới:

>>> class Person:
...     __slots__ = ('name', )
...     def __init__(self, name):
...             self.name = name
...
>>> class Student(Person):
...     pass
...
>>> p = Person('Dat')
>>> p.name
'Dat'
>>> p.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Person' object has no attribute '__dict__'
>>> s = Student('Hai')
>>> s.name
'Hai'
>>> s.__dict__
{}

Chúng ta thấy rằng p không có dictionay, còn s có dictionary, nhưng lưu ý là dictionary này vẫn rỗng không chưa name.
Nếu chúng ta muốn class con cũng chỉ sử dụng slot, chúng ta có thể khai báo như ở dưới:

>>> class Student(Person):
...     __slots__ = tuple() # thêm dòng này
...
>>> s = Student('Test')
>>> s.name
'Test'
>>> s.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Student' object has no attribute '__dict__'
>>>


Chúng ta cũng có thể dùng __slots__ cho class con, chú ý rằng phải thêm attribute mới, không được sử dụng lại attribute cũ.

class Person:
__slots__ = 'name', 

class Student(Person):
__slots__ = 'age', 

Lúc này, instance của class Student sẽ sử dụng slots cho cả nameage.
Chúng ta sẽ thắc mắc rằng, chuyện gì sẽ xảy ra nếu chúng ta khai báo slots cho attribute ở class con lặp lại attribute ở class cha.

class Person:
__slots__ = 'name', 

class Student(Person):
__slots__ = 'name', 'age'

Về cơ bản thì vẫn ổn, nhưng việc sử dụng bộ nhớ sẽ tăng, và nó ghi đè những attribute đã định nghĩa ở class cha, tuy nhiên không nên sử dụng. Chúng ta xem ví dụ ở dưới:

>>> class Person:
...     def __init__(self, name):
...             self._name = name
...     @property
...     def name(self):
...             return self._name.upper()
...     @name.setter
...     def name(self, value):
...             self._name = value
...
>>> class Student(Person):
...     __slots__ = ('name', 'age')
...     def __init__(self, name, age):
...             self.name = name
...             self.age = age
...
>>> p = Person('Dat')
>>> p.name
'DAT'
>>> s = Student('Dat', 35)
>>> s.name
'Dat'

Tới đây, chúng ta thấy rằng chúng ta dùng slots thì attribute sẽ lưu trong slots, còn không dùng thì attribute sẽ được lưu trong dictionary.
Câu hỏi là có cách nào lưu trong cả 2 không? Thực sự thì có, đơn giản gán __dict__ vào __slots__ thôi.

>>> class Person:
...     __slots__ = ('name', '__dict__')
...     def __init__(self, name, age):
...             self.name = name
...             self.age = age
...
>>> p = Person('Dat', 35)
>>> p.name
'Dat'
>>> p.age
35
>>> p.__dict__
{'age': 35}

Chúng ta thấy rằng, name nằm trong slots, age nằm trong dictionary và dictionary nằm trong slot. Chính vì age gián tiếp nằm trong slot nên chúng ta mới gán được self.age = age.
Chúng ta cũng có thể gán attribute mới:

>>> p.school = 'BK'
>>> p.__dict__
{'age': 35, 'school': 'BK'}

Nếu dictionary không nằm trong slots, sẽ báo lỗi:

>>> class Person:
...     __slots__ = ('name',)
...     def __init__(self, name, age):
...             self.name = name
...             self.age = age
...
>>>
>>> p = Person('Dat', 35)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in __init__
AttributeError: 'Person' object has no attribute 'age'



Nội dung về Single Inheritance đã hết, phần 08sẽ giới thiệu về Descriptors.