Python: instance method vs class method vs static method

Bài viết gốc: https://manhhomienbienthuy.bitbucket.io/2017/Oct/25/python-instance-method-vs-class-method-vs-static-method.html (đã xin phép tác giả 😄)

Trong lập trình hướng đối tượng nói chung instance method và class method đều rất quan trọng. Một số ngôn ngữ như Python cung cấp thêm một loại method nữa là static method. Trong bài viết này, chúng ta sẽ tìm hiểu các loại phương này trong ngôn ngữ Python.

Mở đầu

Hãy xem xét ví dụ là một class sau để hiểu rõ hơn về các loại phương thức này:

>>> class Foo:
...     def instance_bar(self, x):
...         print("executing instance_bar(%s, %s)" % (self, x))
...     @classmethod
...     def class_bar(cls, x):
...         print("executing class_bar(%s, %s)" % (cls, x))
...     @staticmethod
...     def static_bar(x):
...         print("executing static_bar(%s)" % x)
...
>>> foo = Foo()

Dưới đây là cách đơn giản nhất để thực thi một phương thức:

>>> foo.instance_bar('args')
executing instance_bar(<__main__.Foo object at 0x7fef22567518>, args)

Đây là một instance method, là phương thức phổ biến nhất. Một đối tượng (instance của class) được ngầm truyền thành tham số thứ nhất (self) của phương thức này.

Class method là phương thức thuộc về cả class. Khi thực thi, nó không dùng đến bất cứ một instance nào của class đó. Thay vào đó, cả class sẽ được truyền thành tham số thứ nhất (cls) của phương thức này:

>>> Foo.class_bar('args')
executing class_bar(<class '__main__.Foo'>, args)

Một điều thú vị là class method cũng có thể gọi từ instance mà không gặp trở ngại gì (nhiều ngôn ngữ vẫn cho làm điều này kèm theo vài warning).

>>> foo.class_bar('args')
executing class_bar(<class '__main__.Foo'>, args)

Static method là một phương thức đặc biệt, nó không sử dụng bất cứ thứ gì liên quan đến class hay instance của class đó. Cả self hay cls đều không xuất hiện trong tham số của loại phương thức này. Và static method hoạt động không khác gì một hàm thông thường.

>>> foo.static_bar('args')
executing static_bar(args)
>>> Foo.static_bar('args')
executing static_bar(args)

Về cơ bản, phương thức cũng giống như hàm. Tuy nhiên, instance_bar là một instance method, và khi bạn gọi foo.instance_bar thì Python sẽ ngầm truyền foo thành tham số thứ nhất của phương thức đó. Và foo.instance_bar không còn là hàm nguyên gốc ban đầu mà là một phiên bản đã được "bind" cho foo.

>>> foo.instance_bar
<bound method Foo.instance_bar of <__main__.Foo object at 0x7f994f8ad748>>

Vì vậy, mặc dù instance_bar cần hai tham số, nhưng foo.instance_bar chỉ cần một tham số thôi.

Tương tự với class method, Foo.class_bar thì class Foo được ngầm gán cho tham số thứ nhất của class_bar. Ngay cả khi gọi từ instance, foo.class_bar thì class Foo vẫn được gán cho class_bar.

>>> Foo.class_bar
<bound method Foo.class_bar of <class '__main__.Foo'>>
>>> foo.class_bar
<bound method Foo.class_bar of <class '__main__.Foo'>>

Riêng static method là trường hợp đặc biệt, mặc dù nó là một phương thức nhưng dù gọi foo.static_bar hay Foo.static_bar thì kết quả trả về vẫn là hàm ban đầu không hề được "bind" bất cứ một đối tượng nào.

>>> foo.static_bar
<function Foo.static_bar at 0x7f994f892b70>
>>> Foo.static_bar
<function Foo.static_bar at 0x7f994f892b70>

Python đã lưu trữ vào thực hiện tất cả việc "bind" này như thế nào?

Trước hết, toàn bộ các phương thức này đều được lưu trữ trong __dict__ của class.

>>> Foo.__dict__
mappingproxy({'__weakref__': <attribute '__weakref__' of 'Foo' objects>, '__module__': '__main__', 'class_bar': <classmethod object at 0x7f994f8ad6d8>, '__doc__': None, 'static_bar': <staticmethod object at 0x7f994f8ad710>, '__dict__': <attribute '__dict__' of 'Foo' objects>, 'instance_bar': <function Foo.instance_bar at 0x7f994f892a60>})

Vì vậy, mọi tham chiếu đến các loại phương này đều sẽ thông qua __dict__.

Mặt khác, Python có một loại đối tượng rất thú vị, thích hợp để áp dụng cho những đối tượng cần "bind" gọi là descriptor. Hiểu đơn giản, descriptor là những đối tượng có ít nhất một trong các phương thức sau:

  • __get__
  • __set__
  • __delete__

Bằng việc cài đặt các phương thức này, một đối tượng có thể thay đổi hoạt động của chúng khi được tham chiếu.

Và các phương thức chính là descriptor. Chúng ta những descriptor đặc biệt, chỉ có phương thức __get__, và cũng nhờ phương thức này là hoạt động của chúng cũng rất đặc biệt.

Khi chúng ta định nghĩa một class, các phương thức được định nghĩa như function thông thường, nhưng Python tự thêm __get__ cho chúng để có thể gọi về sau. Nhờ đó, các phương thức khi được gọi sẽ có thể được "bind" các đối tượng tương ứng.

Hãy nhìn lại một chút về quá trình tham chiếu thuộc tính của instance.

objects attribute lookup

Theo như sơ đồ, khi chúng ta gọi foo.instance_bar chúng ta sẽ nhận được Foo.__dict__['instance_bar'].__get__(foo, Foo)

>>> foo.instance_bar
<bound method Foo.instance_bar of <__main__.Foo object at 0x7f994f8ad748>>
>>> Foo.__dict__['instance_bar'].__get__(foo, Foo)
<bound method Foo.instance_bar of <__main__.Foo object at 0x7f994f8ad748>>

Và dưới đây là quá trình tham chiếu thuộc tính của class.

classes attribute lookup

Và, khi chúng ta gọi Foo.class_bar chúng ta sẽ nhận được Foo.__dict__['class_bar'].__get__(None, Foo)

>>> Foo.class_bar
<bound method Foo.class_bar of <class '__main__.Foo'>>
>>> Foo.__dict__['class_bar'].__get__(None, Foo)
<bound method Foo.class_bar of <class '__main__.Foo'>>

Việc tham chiếu khá đơn giản, tuy nhiên, mọi sự vi diệu của các phương thức đều nhờ __get__ mà ra cả. Nó vi diệu tới mức chúng ta có thể gọi instance method từ class mà không gặp vấn đề gì.

>>> Foo.instance_bar(1, 2)
executing instance_bar(1, 2)

Tất nhiên là trong lập trình hướng đối tượng, chắc không ai làm trò này.

Instance method

Instance method có phương thức __get__ và nó sẽ ngầm truyền tham số thứ nhất của __get__ thành tham số thứ nhất của phương thức. Chúng ta có thể minh họa quá trình này với đoạn code như sau:

class Function:
    "Simulate func_descr_get() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        if obj is None:
            return self.f
        def newfunc(*args):
            return self.f(obj, *args)
        return newfunc

Chúng ta có thể nhìn rõ quá trình thực thi __get__ như sau:

>>> # Truy cập thông qua __dict__ không gọi __get__
... Foo.__dict__['instance_bar']
<function Foo.instance_bar at 0x7f994f892a60>
>>> # Goị instance từ class có thực hiện tham chiếu và gọi __get__
... # Nhưng vì truyền None cho tham số thứ nhất nên kết quả vẫn là hàm ban đầu
... Foo.instance_bar
<function Foo.instance_bar at 0x7f994f892a60>
>>> # Truy cập từ instance sẽ gọi phương thức __get__
... # và kết quả là hàm đã được bind
... foo.instance_bar
<bound method Foo.instance_bar of <__main__.Foo object at 0x7f994f8ad748>>
>>> # hàm được bind vẫn giữ các giá trị hàm ban đầu
... foo.instance_bar.__func__
<function Foo.instance_bar at 0x7f994f892a60>

Class method

Class method ngầm gán class cho tham số thứ nhất khi gọi hàm. Và điều này luôn đúng dù nó được gọi từ instance hay class.

>>> Foo.class_bar('args')
executing class_bar(<class '__main__.Foo'>, args)
>>> foo.class_bar('args')
executing class_bar(<class '__main__.Foo'>, args)

Sự vi diệu này có được nhờ decorator @classmethod. Chính decorator này đã biến đổi phương thức __get__ của class method. Một minh họa cho phương thức __get__ như sau:

class ClassMethod(object):
    "Emulate PyClassMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, klass=None):
        if klass is None:
            klass = type(obj)
        def newfunc(*args):
            return self.f(klass, *args)
        return newfunc

Static method

Static method là phương thức không phụ thuộc gì vào instance hay class. Vì vậy, phương thức __get__ của nó khá đơn giản, nó chỉ đơn giản trả về bản thân hàm đó là đủ.

class StaticMethod(object):
    "Emulate PyStaticMethod_Type() in Objects/funcobject.c"

    def __init__(self, f):
        self.f = f

    def __get__(self, obj, objtype=None):
        return self.f

Khi nào nên dùng static method

Static method, với sự đặc biệt của nó, được dùng rất hạn chế. Bởi vì nó không giống như instance method hay class method, nó không có bất cứ sự liên quan tới đối tượng gọi nó. Static method không phải là phương thức được sử dụng thường xuyên.

Tuy nhiên, nó vẫn tồn tại là có lý do của nó. Static method thường dùng để nhóm các hàm tiện ích lại với nhau (trong cùng một class). Nó không yêu cầu gì từ class đó, ngoại trừ việc tham chiếu khi được gọi.

Nhưng hàm như vậy không cho vào class nào cũng không vấn đề gì. Nhưng nhóm chúng trong class và gọi chúng thông quan instance hoặc class sẽ giúp chúng ta hiểu hơn về bối cảnh cũng như chức năng của chúng.

Nếu bạn cảm thấy những lợi ích trên từ việc sử dụng static method, hãy sử dụng nó. Còn không, hãy dùng hàm và module như thông thường.

Kết luận

Instace method, class method, static method là những khái niệm rất cơ bản trong lập trình hướng đối tượng với Python. Nắm vững khái niệm cũng như bối cảnh sử dụng chúng sẽ giúp chúng ta code đẹp hơn rất nhiều.