Chuyển đến nội dung chính

Giới thiệu về Magic Methods trong Python

Khám phá magic methods là gì đồng thời xem sự khác biệt giữa method và function.
· 11 phút đọc

Khám phá cùng AI

Một trong những yếu tố góp phần khiến Python trở thành ngôn ngữ lập trình mạnh mẽ là các magic methods. Trong một số cộng đồng Pythonista, bạn có thể nghe chúng được gọi là ‘dunder methods’ do cấu trúc hai dấu gạch dưới. 

Vai trò của chúng trong Python là thực hiện một quy trình gọi là ‘quá tải toán tử’ (operator overloading), nghĩa là một toán tử được định nghĩa sẵn được gán thêm ý nghĩa mở rộng. Nói cách khác, operator overloading được dùng để bổ sung hành vi tùy chỉnh cho các lớp (class) của chúng ta nhằm cho phép chúng hoạt động với các toán tử và chức năng dựng sẵn của Python.  

Trong bài viết này, chúng ta sẽ đi sâu hơn vào magic methods trong Python bằng cách: 

  • Phân biệt sự khác nhau giữa method và function 
  • Giải thích magic methods là gì
  • Cung cấp một số ví dụ Python. 

Xem sổ tay DataLab này để làm theo đoạn mã dùng trong hướng dẫn. 

Phương thức (Method) và Hàm (Function) trong Python

Trước khi tìm hiểu magic methods là gì, chúng ta cần hiểu sự khác nhau giữa method và function trong Python. 

Python dùng method để biểu diễn hành vi của một đối tượng (object). Vì vậy, method gắn với một đối tượng và sẽ thực hiện một số thao tác trên dữ liệu bên trong đối tượng đó. Một yếu tố phân biệt quan trọng khác của method là tham số self phải luôn xuất hiện như tham số đầu tiên được truyền vào method. 

Lưu ý: Tham số đầu tiên không bắt buộc phải đặt tên là self, nhưng cộng đồng Pythonista thống nhất đây là thực hành tốt nhất. 

# Example of a method
class Human:
    # This is a method
    def walk(self):
        print("I am walking") 

Các method phụ thuộc vào class mà chúng gắn với. Do đó, để gọi một method, cần tạo một thể hiện (instance) của class trước khi có thể truy cập method bằng toán tử chấm. 

# This will work
joe = Human()
joe.walk()

"""
I am walking
"""




# This will throw a NameError
walk()

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_153/2327344440.py in <module>
      4
      5 # This will throw a NameError
----> 6 walk()

NameError: name 'walk' is not defined

Một function thì khác: chúng ta không phải tạo instance của class. 

Hàm trong Python là các bước được thực thi như một khối mã chỉ khi được gọi. Chúng được định nghĩa độc lập với bất kỳ đối tượng class nào, nghĩa là có thể gọi trực tiếp bằng tên. Vì không gắn với class nào, chúng ta không cần truyền self làm tham số – các tham số là tùy chọn. 

Tóm lại, function đơn giản là một tập hợp các chỉ dẫn được xác định rõ ràng mà trình thông dịch dùng để thực hiện một tác vụ. 

# Example of a function
def run():
  print("I am running.")
​
run()

"""
I am running.
"""

Như đã trình bày trong bài Cách gọi và viết hàm, function cũng có hai dạng: dựng sẵn và do người dùng định nghĩa. 

  • Hàm dựng sẵn: Các hàm có trong thư viện chuẩn của Python. 
  • Hàm do người dùng định nghĩa: Các hàm do người dùng định nghĩa – Python cho phép người dùng tự định nghĩa và sử dụng hàm của riêng mình để truy cập trong chương trình Python.  

Ở nhiều khía cạnh, method và function giống nhau – ví dụ, cả hai đều được định nghĩa bằng câu lệnh def. Điểm khác biệt cốt lõi là method gắn với một đối tượng và dùng để thao tác trên dữ liệu nằm trong một class, trong khi function được định nghĩa độc lập và có thể được gọi bằng tên ở bất cứ đâu trong chương trình. 

Python magic methods là gì?

Ở phần đầu hướng dẫn, chúng tôi đề cập rằng magic methods còn được gọi là dunder methods do cấu trúc hai dấu gạch dưới. Ý chúng tôi là tất cả magic methods trong Python đều bắt đầu và kết thúc bằng hai dấu gạch dưới. 

Magic method nổi tiếng nhất là phương thức __init__, cho phép khởi tạo một class với các thuộc tính cụ thể. 

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

Để tạo một instance của class này, chúng ta phải chỉ định các thuộc tính name, age và role của instance. Ví dụ:

user_1 = Employee(name="John", age=24, role="Data Scientist")

Nếu không, sẽ phát sinh TypeError. 

Phương thức __init__ thuộc nhóm magic methods khởi tạo và dựng (initialization and construction), nhưng nó chỉ là một trong số nhiều phương thức. Bảng dưới đây nêu một số magic methods khác thuộc nhóm khởi tạo và dựng. 

__new__(cls, other)

Dùng để tạo một instance mới của class

__init__(self, other)

Được gọi sau khi một instance mới được tạo; Tất cả thuộc tính truyền vào biểu thức khởi tạo class sẽ được dùng làm thuộc tính. 

__del__(self)

Còn gọi là destructor; Được gọi khi một instance sắp bị hủy. 

Cách đơn giản nhất để hình dung magic methods là một bản hợp đồng giữa phần triển khai của bạn và trình thông dịch Python. Một trong các điều khoản yêu cầu Python thực hiện một số hành động ngầm khi có những điều kiện nhất định, chẳng hạn như khởi tạo một instance của class với các đối tượng cụ thể. 

Nói cách khác, bạn không có trách nhiệm gọi trực tiếp magic method – Python sẽ làm điều đó cho bạn ở hậu trường. 

Các phương thức biểu diễn chuỗi

Python là một ngôn ngữ lập trình hướng đối tượng (OOP) – mọi thứ trong Python đều là đối tượng. Khi một đối tượng mới được tạo, chúng ta ngầm tạo ra một đối tượng liên quan vì mọi class đều kế thừa từ object.  

Lưu ý: bạn cũng có thể làm cho việc kế thừa trở nên tường minh bằng cách truyền class object vào class của bạn. 

# This works
class Car:
  pass
​
# This also works
class Vehicle(object):
  pass
​
car = Car()
vehicle = Vehicle()
​
print(car, vehicle, sep="\n")

"""
<__main__.Car object at 0x7f80449dc0a0>
<__main__.Vehicle object at 0x7f80449dc1f0>
"""

Trong đoạn mã trên, chúng ta tạo hai class rỗng, nhưng trình thông dịch Python vẫn biết in gì khi gọi lệnh print. Điều này khả thi vì mọi class đều kế thừa từ class object (lớp cha của mọi class trong Python), và nó có một magic method tên là __repr__, mà lệnh print sẽ gọi để trả về một giá trị cho chương trình chính của chúng ta.  

Những ai quen thuộc với kế thừa sẽ biết rằng một lớp con có thể ghi đè lớp cha bằng cách định nghĩa method cùng tên của riêng mình. 

# Overriding the object __repr__
class Vehicle:
  def __repr__(self):
      return f"{self.__class__.__qualname__}"
​
vehicle = Vehicle()
print(vehicle)

"""
Vehicle
"""

Bạn cũng có thể dùng magic method __str__ để tạo biểu diễn chuỗi được gọi bằng lệnh print. 

Thực tế, magic method __repr__ đóng vai trò dự phòng cho magic method __str__. Vì vậy, khi chúng ta gọi lệnh print, Python sẽ tìm __str__ được định nghĩa trong class của bạn trước, rồi mới quay về __repr__ nếu không có. 

# Using __str__ for string representation
class Vehicle:
  def __str__(self):
      return f"{self.__class__.__qualname__}"
     
vehicle = Vehicle()
print(vehicle)

"""
Vehicle
"""

Bảng dưới đây liệt kê một số magic methods khác liên quan đến biểu diễn chuỗi. 

__str__(self)

Được gọi bởi hàm dựng sẵn str() của Python và bởi lệnh print để hiển thị biểu diễn chuỗi không chính thức của một đối tượng. Lưu ý rằng nó không bắt buộc phải là một biểu thức Python hợp lệ. 

__repr__(self)

Được gọi bởi hàm dựng sẵn repr() của Python và các phép chuyển đổi chuỗi để hiển thị biểu diễn chuỗi chính thức của một đối tượng. Lưu ý rằng biểu diễn này nên trông giống một biểu thức Python hợp lệ, nhưng nếu không thể, một chuỗi mô tả hữu ích vẫn được ưu tiên. 

__unicode__(self)

Được gọi để triển khai hàm dựng sẵn unicode() của Python và trả về một đối tượng Unicode. 

__hash__(self)

Được gọi bởi hàm dựng sẵn hash() của Python và các phép toán trên phần tử của tập hợp băm. Phương thức nên trả về một số nguyên. 

__nonzero__(self)

Được gọi bởi phép toán dựng sẵn bool() của Python để kiểm tra giá trị chân lý. Phương thức nên trả về True hoặc False (hoặc tương đương số nguyên 1 hoặc 0). 

Các phương thức “ma thuật” về đóng gói (Encapsulation) 

Đóng gói là một trong những khái niệm nền tảng trong lập trình hướng đối tượng (OOP). Nó đề cập đến việc gom dữ liệu cùng với các phương thức thao tác trên dữ liệu đó, hoặc hạn chế truy cập trực tiếp vào một số thành phần nhất định trong một đối tượng.

Các lập trình viên đến từ những ngôn ngữ OOP khác thường tranh luận về việc Python không thực sự ôm trọn khái niệm đóng gói cho class. Họ đã quen với các bộ sửa đổi (modifier) tường minh cho method hoặc field, cho phép định nghĩa những thứ như thuộc tính private với getter và setter public. Khác biệt ở Python là phần lớn cơ chế đóng gói sẵn có diễn ra thông qua magic methods. 

Ví dụ, __setattr__ là một giải pháp đóng gói cho phép chúng ta định nghĩa hành vi khi gán giá trị cho một thuộc tính dù thuộc tính đó có tồn tại hay không. Nói cách khác, có thể định nghĩa các quy tắc cho việc thay đổi giá trị của thuộc tính. 

import string

class Alphabet():
    def __setattr__(self, name, value):
        self.__dict__[name] = value.upper()

a = Alphabet()

# Assinging an attribute that does not exist
# The setattr should convert it to caps despite being set as lowercase
a.alphabet = string.ascii_lowercase
print(a.alphabet)

Trong đoạn mã trên, chúng ta tạo một class tên Alphabet không có thuộc tính nào, nhưng chúng ta bảo Python rằng muốn viết hoa bất kỳ thuộc tính nào của instance thuộc class này khi đặt giá trị.

Chúng tôi hiểu OOP có thể hơi khó tiếp cận với một số độc giả. Để bắt kịp OOP trong Python, hãy xem Lộ trình Python Programmer của DataCamp.   

Các magic methods về toán học

Một nhóm magic methods khác trong Python là các magic methods về toán học (còn gọi là các toán tử số học thông thường). Biểu thức được tạo bằng các ký hiệu đặc biệt gọi là ‘toán tử’ trong Python. Việc dùng các toán tử như vậy trong một biểu thức liên quan đến việc tạo các đối tượng có chứa magic methods về toán học. Không tuân thủ quy tắc này sẽ làm phát sinh TypeError. 

class Numbers:
  def __init__(self, a, b):
      self.a = a
      self.b = b
   
set_a = Numbers(2, 4)
set_b = Numbers(3, 5)
​
print(set_a + set_b)
"""
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_152/3639634723.py in <module>
      7 set_b = Numbers(3, 5)
      8
----> 9 print(set_a + set_b)

TypeError: unsupported operand type(s) for +: 'Numbers' and 'Numbers'
"""

Trong đoạn mã trên, chúng ta thử tạo nhiều instance của class Numbers và cộng chúng lại, dẫn đến lỗi như dự kiến. Một cách giải quyết là tạo một method trong class để cộng các số lại, nhưng cách này sẽ ngăn chúng ta tạo biểu thức. 

Một giải pháp tốt hơn là triển khai magic method __add__ như sau:  

# Using add operator
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
b = Numbers(3, 5)
print(a + b) # Should result in Numbers(5, 9)


"""
RandomNumbers(5, 9)
"""

Ví dụ mã trên chỉ cho phép cộng hai đối tượng Numbers với nhau. Hãy triển khai thêm một magic method về toán học để cho phép nhân một đối tượng Numbers với một số nguyên. 

# Multiplying Numbers with an integer
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __mul__(self, other):
      if not isinstance(other, int):
          return NotImplemented
         
      return Numbers(self.a * other, self.b * other)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
print(a * 5) # Should return 10, 20




"""
RandomNumbers(10, 20)
"""

Trong đoạn mã trên, chúng ta thêm magic method __mul__ để cho phép nhân các số của class tùy chỉnh với một số nguyên. 

Lưu ý đối tượng Numbers nằm ở phía bên trái của toán tử * – điều này không phải ngẫu nhiên. Hãy đặt nó ở bên phải toán hạng và xem điều gì xảy ra. 

- snip --

a = Numbers(2, 4)
print(3 * a)
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/tmp/ipykernel_152/2857655049.py in <module>
      1 a = Numbers(2, 4)
----> 2 print(3 * a)

TypeError: unsupported operand type(s) for *: 'int' and 'Numbers'

Python ném ra TypeError. 

Magic method __mul__ chúng ta triển khai chỉ xử lý trường hợp đối tượng Numbers ở phía bên trái toán tử *. Để mở rộng chức năng này, chúng ta phải thêm magic method đảo ngược, __rmul__. 

# Enabling reverse multiplication
class Numbers:
    def __init__(self, a, b):
        self.a = a
        self.b = b
   
    def __add__(self, other):
        # Only permit Numbers objects to be added
        if not isinstance(other, Numbers):
            return NotImplemented
        return Numbers(other.a + self.a, other.b + self.b)
   
    def __mul__(self, other):
      if not isinstance(other, int):
          return NotImplemented
      return Numbers(self.a * other, self.b * other)

    def __rmul__(self, other):
        return self.__mul__(other)
   
    def __repr__(self):
      return f"{self.__class__.__qualname__}({self.a}, {self.b})"
   
a = Numbers(2, 4)
print(5 * a) # Should return 10, 20

"""
Numbers(10, 20)
"""

Giờ đây, chúng ta có thể tạo biểu thức nhân mà không cần bận tâm đối tượng Numbers nằm ở phía nào của toán hạng. 

Tổng kết

Nguyên tắc cơ bản để tận dụng magic methods trong Python vẫn giữ nguyên: tạo một class và ghi đè magic method của class object. Có rất nhiều magic methods trong Python, nghĩa là hướng dẫn sẽ cực kỳ dài nếu chúng ta quyết định đề cập từng cái một. Chúng tôi khuyến nghị bạn xem tài liệu Python để có cái nhìn tổng quan về toàn bộ phạm vi các magic methods bạn có thể dùng để nâng cao kỹ năng lập trình Python.  



Chủ đề