12/08/2018, 16:09

Method overriding trong Python

Trong bài viết này tôi xin được tập hợp lại 1 bài số kiến thức về Method override trong python. Trước hết chúng ta hãy cũng tìm hiểu xem Override hay còn gọi là ghi đè là gì ? Ghi đè là khả năng của Class có thể thay đổi hành động của method được cung cấp bởi một trong những class mà nó kế ...

Trong bài viết này tôi xin được tập hợp lại 1 bài số kiến thức về Method override trong python.

Trước hết chúng ta hãy cũng tìm hiểu xem Override hay còn gọi là ghi đè là gì ? Ghi đè là khả năng của Class có thể thay đổi hành động của method được cung cấp bởi một trong những class mà nó kế thừa.

Ghi đè là một phần rất quan trọng của OOP vì nó giúp lập trình viên khai thác hết sức mạnh của tính kế thừa. Thông qua phương thức overriding một Class có thể "sao chép" Class khác, tránh trùng lặp code, và đồng thời tăng cường khả năng tùy biến một phần code. Ghi đè là một phần quan trọng của cơ chế kế thừa trong OOP.

Lướt nhanh về tính kế thừa trong Python

Tương tự như hầu hết các ngôn ngữ OOP, trong Python tính kế thừa cũng được thể hiện thông qua implicit delegation (khai báo ko tường minh): Khi ta gọi 1 method của đối tượng, nếu nó ko có method đó, việc gọi method sẽ được chuyển tới các Class mà nó đang kế thừa theo các quy tắc ngôn ngữ cụ thể trong trường hợp đa thừa kế.

Ví dụ:

class Parent(object):
    def __init__(self):
        self.value = 5

    def get_value(self):
        return self.value

class Child(Parent):
    pass

Như bạn có thể thấy class Child là rỗng, nhưng kể từ khi nó thừa kế từ Parent, Python có trách nhiệm định tuyến tất cả các lệnh gọi phương thức (method). Vì vậy bạn có thể sử dụng phương thức get_value() của đối tượng Child và mọi thứ hoạt động như mong đợi.

>>> c = Child()
>>> c.get_value()
5

Thật vậy get_value() không phải là một phần của lớp Child như thể nó được định nghĩa trong nó

>>> p = Parent()
>>> c = Child()
>>>
>>> dir(p)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
 '__getattribute__', '__hash__', '__init__', '__module__', '__new__',
 '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
 '__str__', '__subclasshook__', '__weakref__', 'get_value', 'value']
>>>
>>> dir(c)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
 '__getattribute__', '__hash__', '__init__', '__module__', '__new__',
 '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
 '__str__', '__subclasshook__', '__weakref__', 'get_value', 'value']
>>>
>>> dir(Parent)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
 '__getattribute__', '__hash__', '__init__', '__module__', '__new__',
 '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
 '__str__', '__subclasshook__', '__weakref__', 'get_value']
>>>
>>> dir(Child)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__',
 '__getattribute__', '__hash__', '__init__', '__module__', '__new__',
 '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
 '__str__', '__subclasshook__', '__weakref__', 'get_value']
>>>
>>> Parent.__dict__
dict_proxy({'__module__': '__main__',
            'get_value': <function get_value at 0xb69a656c>,
            '__dict__': <attribute '__dict__' of 'Parent' objects>,
            '__weakref__': <attribute '__weakref__' of 'Parent' objects>,
            '__doc__': None,
            '__init__': <function __init__ at 0xb69a6534>})
>>>
>>> Child.__dict__
dict_proxy({'__module__': '__main__', '__doc__': None})

Điều này cho thấy rằng lớp Child không thực sự chứa phương thức get_value(). Để có một cái nhìn sâu sắc về cơ chế này bạn có thể đọc thêm tại đây.

Method overriding trong thực tiễn

Trong Python, việc ghi đè xảy ra chỉ đơn giản là khi trong lớp con tồn tại một phương thức có cùng tên với phương thức trong lớp cha. Khi bạn định nghĩa một phương thức trong đối tượng mà bạn thực hiện điều này sau đó có thể thỏa mãn method call, khi đó các hành động được định nghĩa trong method của lớp cha sẽ không diễn ra.

class Parent(object):
    def __init__(self):
        self.value = 5

    def get_value(self):
        return self.value

class Child(Parent):
    def get_value(self):
        return self.value + 1

Child objects sẽ có hành vi get_value() khác với class Parent đã định nghĩa

>>> c = Child()
>>> c.get_value()
6

và chỉ cần quan sát kỹ các class chúng ta thấy một sự khác biệt

>>> Parent.__dict__
dict_proxy({'__module__': '__main__',
            'get_value': <function get_value at 0xb69a656c>,
            '__dict__': <attribute '__dict__' of 'Parent' objects>,
            '__weakref__': <attribute '__weakref__' of 'Parent' objects>,
            '__doc__': None,
            '__init__': <function __init__ at 0xb69a6534>})
>>>
>>> Child.__dict__
dict_proxy({'__module__': '__main__',
            'get_value': <function get_value at 0xb69a65a4>,
            '__doc__': None})

kể từ thời điểm này, lớp Child thực sự chứa một phương thức get_value() với một implementation hoàn toàn khác (id của hai function là khác nhau).

Khi bạn ghi đè bạn phải nghĩ rằng nếu bạn muốn filter các đối số cho việc implement ban đầu, hay nếu bạn muốn lọc kết quả, hoặc cả hai. Bạn thường muốn lọc các đối số (pre-filter) nếu bạn muốn thay đổi dữ liệu mà việc implement của class cha sẽ xử lý. Trong khi bạn lọc kết quả (post-filter) nếu bạn muốn thêm một lớp xử lý bổ sung. Rõ ràng cả hai điều có thể được thực hiện cùng nhau trong cùng một method. Vì bạn phải gọi một cách rõ ràng method của Parent class, bạn có thể tự do làm nó ở nơi bạn muốn trong code của method mới: quyết định về loại filter mà bạn muốn có tác động đến thứ tự method call.

Ví dụ về pre-filtering

import datetime

class Logger(object):
    def log(self, message):
        print message

class TimestampLogger(Logger):
    def log(self, message):
        message = "{ts} {msg}".format(ts=datetime.datetime.now().isoformat(),
                                      msg=message)
        super(TimestampLogger, self).log(message)

TimestampLogger object đã thêm các thông tin vào message string trước khi gọi tới tới original implementation trong method log() của class Logger.

>>> l = Logger()
>>> l.log('hi!')
hi!
>>>
>>> t = TimestampLogger()
>>> t.log('hi!')
2014-05-19T13:18:53.402123 hi!
>>> 

Ví dụ về post-filtering

import os

class FileCat(object):
    def cat(self, filepath):
        f = file(filepath)
        lines = f.readlines()
        f.close()
        return lines

class FileCatNoEmpty(FileCat):
    def cat(self, filepath):
        lines = super(FileCatNoEmpty, self).cat(filepath)
        nonempty_lines = [l for l in lines if l != '
']
        return nonempty_lines

Khi bạn sử dụng FileCatNoEmpty object bạn sẽ thu đc được kết quả từ FileCat object là các dòng empty sẽ bị loại bỏ khỏi File.

Dễ thấy rắng trong ví dụ đầu tiên thì original implementation được gọi sau cùng, trong khi ở ví dụ thứ 2, nó được gọi trước tiên. Do đó không có vị trí cố định cho việc gọi original method, và nó phụ thuộc vào những gì bạn muốn làm.

Có nên luôn luôn gọi super()?

Chúng ta có nên luôn luôn gọi original method implementation ? Theo lý thuyết, API được thiết kế tốt nên luôn luôn có thể nhưng chúng ta biết rằng luôn tồn tai các trường hợp biên: method gốc có thể có tác dụng phụ nếu bạn muốn tránh và đôi khi API không thể tránh chúng. Trong những trường hợp này, bạn có thể bỏ qua việc gọi đến original method; Python không bắt buộc việc gọi method gốc, do đó tất cả sự lựa chọn đều nằm ở bạn. Hãy chắc chắn để biết bạn đang làm gì, tuy nhiên hãy note lại tại sao bạn lại ghi đè hoàn toàn một method.

Tổng kết

  • Hãy cố gắng sử dụng, gọi lại method gốc nếu có thể. Điều này làm các API hoạt động ổn định nhất, This meakes the underlying API work as expected. Khi cần phải bỏ qua việc gọi method gốc, hãy chắc chắn đã ghi lại các lý do.
  • Luôn luôn sử dụng super(cls, self) cho Python 2.x hoặc super() cho Python 3.x để gọi original implementation method. Điều này đảm bảo trật tự của việc phân giải method trong trường hợp đa kế thừa và đối với Python 3.x, bảo vệ khỏi những thay đổi trong hệ thống phân cấp.
  • Nếu bạn gọi đến original method, hãy làm điều đó ngay khi bạn có tất cả dữ liệu bạn cần thiết để chạy nó.

Tham khảo: http://blog.thedigitalcatonline.com/blog/2014/05/19/method-overriding-in-python/#.Wc3anBOCyHo

0