Sự khác biệt giữa phương thức `classmethod` và metaclass là gì?


12

Trong Python, tôi có thể tạo một phương thức lớp bằng cách sử dụng @classmethodtrang trí:

>>> class C:
...     @classmethod
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> C.f()
f called with cls=<class '__main__.C'>

Ngoài ra, tôi có thể sử dụng một phương thức (ví dụ) bình thường trên siêu dữ liệu:

>>> class M(type):
...     def f(cls):
...             print(f'f called with cls={cls}')
...
>>> class C(metaclass=M):
...     pass
...
>>> C.f()
f called with cls=<class '__main__.C'>

Như thể hiện bởi đầu ra của C.f(), hai cách tiếp cận này cung cấp chức năng tương tự nhau.

Sự khác biệt giữa việc sử dụng @classmethodvà sử dụng một phương pháp bình thường trên siêu dữ liệu là gì?

Câu trả lời:


5

Vì các lớp là các thể hiện của siêu dữ liệu, không có gì bất ngờ khi một "phương thức cá thể" trên siêu dữ liệu sẽ hoạt động giống như một lớp đối xứng.

Tuy nhiên, vâng, có những khác biệt - và một số trong số chúng còn hơn cả ngữ nghĩa:

  1. Sự khác biệt quan trọng nhất là một phương thức trong siêu dữ liệu không "hiển thị" từ một thể hiện của lớp . Điều đó xảy ra bởi vì tra cứu thuộc tính trong Python (theo cách đơn giản hóa - mô tả có thể được ưu tiên) tìm kiếm một thuộc tính trong thể hiện - nếu nó không có trong cá thể, thì Python sẽ tìm trong lớp của cá thể đó, và sau đó tìm kiếm tiếp tục các siêu lớp của lớp, nhưng không phải trên các lớp của lớp. Stdlib Python sử dụng tính năng này trongabc.ABCMeta.register phương thức. Tính năng đó có thể được sử dụng tốt, vì các phương thức liên quan đến chính lớp được tự do sử dụng lại làm thuộc tính cá thể mà không có bất kỳ xung đột nào (nhưng một phương thức vẫn sẽ xung đột).
  2. Tuy nhiên, một sự khác biệt khác là một phương thức được khai báo trong siêu dữ liệu có thể có sẵn trong một số lớp, không liên quan đến nhau - nếu bạn có hệ thống phân cấp lớp khác nhau, hoàn toàn không liên quan đến cái gì chúng xử lý, nhưng muốn có một số chức năng chung cho tất cả các lớp , bạn phải đưa ra một lớp mixin, sẽ phải được đưa vào làm cơ sở trong cả hai cấu trúc phân cấp (giả sử bao gồm tất cả các lớp trong sổ đăng ký ứng dụng). (NB. Mixin đôi khi có thể là một cuộc gọi tốt hơn so với siêu dữ liệu)
  3. Classmethod là một đối tượng "classmethod" chuyên biệt, trong khi một phương thức trong metaclass là một hàm thông thường.

Vì vậy, điều xảy ra là cơ chế mà classmethod sử dụng là " giao thức mô tả ". Trong khi các hàm bình thường có một __get__phương thức sẽ chèn selfđối số khi chúng được truy xuất từ ​​một thể hiện và để trống đối số đó khi được truy xuất từ ​​một lớp, một classmethodđối tượng có một đối tượng khác__get__ , nó sẽ chèn chính lớp đó ("chủ sở hữu") làm tham số đầu tiên trong cả hai tình huống.

Điều này không tạo ra sự khác biệt thực tế trong hầu hết thời gian, nhưng nếu bạn muốn truy cập vào phương thức như một hàm, với mục đích thêm động trang trí vào nó hoặc bất kỳ phương thức nào khác, cho một phương thức trong siêu dữ liệu meta.methodlấy ra hàm, sẵn sàng sử dụng , trong khi bạn phải sử dụng cls.my_classmethod.__func__ để truy xuất nó từ một classmethod (và sau đó bạn phải tạo một classmethodđối tượng khác và gán lại cho nó, nếu bạn thực hiện một số gói).

Về cơ bản, đây là 2 ví dụ:


class M1(type):
    def clsmethod1(cls):
        pass

class CLS1(metaclass=M1):
    pass

def runtime_wrap(cls, method_name, wrapper):
    mcls = type(cls)
    setattr(mcls, method_name,  wrapper(getatttr(mcls, method_name)))

def wrapper(classmethod):
    def new_method(cls):
        print("wrapper called")
        return classmethod(cls)
    return new_method

runtime_wrap(cls1, "clsmethod1", wrapper)

class CLS2:
    @classmethod
    def classmethod2(cls):
        pass

 def runtime_wrap2(cls, method_name, wrapper):
    setattr(cls, method_name,  classmethod(
                wrapper(getatttr(cls, method_name).__func__)
        )
    )

runtime_wrap2(cls1, "clsmethod1", wrapper)

Nói cách khác: ngoài sự khác biệt quan trọng mà một phương thức được định nghĩa trong siêu dữ liệu có thể nhìn thấy từ thể hiện và một classmethodđối tượng thì không, sự khác biệt khác, trong thời gian chạy sẽ có vẻ mơ hồ và vô nghĩa - nhưng điều đó xảy ra vì ngôn ngữ không cần phải đi theo cách của nó với các quy tắc đặc biệt cho phân loại: Cả hai cách khai báo phân loại đều có thể, do hậu quả của thiết kế ngôn ngữ - một, vì thực tế là một lớp tự nó là một đối tượng, và một khả năng khác, như một khả năng của nhiều người, việc sử dụng giao thức mô tả cho phép một người chuyên truy cập thuộc tính trong một thể hiện và trong một lớp:

Nội dung classmethodđược xác định trong mã gốc, nhưng nó chỉ có thể được mã hóa bằng python thuần và sẽ hoạt động theo cùng một cách chính xác. Dưới đây là lớp 5 dòng có thể được sử dụng như một công cụ classmethodtrang trí không có sự khác biệt về thời gian chạy so với repr` @classmethod" at all (though distinguishable through introspection such as calls toisinstance , and evenrepr` tích hợp):


class myclassmethod:
    def __init__(self, func):
        self.__func__ = func
    def __get__(self, instance, owner):
        return lambda *args, **kw: self.__func__(owner, *args, **kw)

Và, ngoài các phương thức, điều thú vị là hãy nhớ rằng các thuộc tính chuyên biệt như @propertytrên siêu dữ liệu sẽ hoạt động như các thuộc tính lớp chuyên biệt, giống nhau, không có hành vi đáng ngạc nhiên nào cả.


2

Khi bạn diễn đạt nó giống như bạn đã làm trong câu hỏi, @classmethodvà siêu dữ liệu có thể trông giống nhau nhưng chúng có mục đích khá khác nhau. Lớp được chèn trong @classmethodđối số 'thường được sử dụng để xây dựng một thể hiện (tức là một hàm tạo thay thế). Mặt khác, siêu dữ liệu thường được sử dụng để sửa đổi chính lớp đó (ví dụ như những gì Django làm với các mô hình DSL của nó).

Điều đó không có nghĩa là bạn không thể sửa đổi lớp bên trong một lớp. Nhưng sau đó, câu hỏi trở thành lý do tại sao bạn không định nghĩa lớp theo cách bạn muốn sửa đổi nó ngay từ đầu? Nếu không, nó có thể đề xuất một bộ cấu trúc lại để sử dụng nhiều lớp.

Hãy mở rộng ví dụ đầu tiên một chút.

class C:
    @classmethod
    def f(cls):
        print(f'f called with cls={cls}')

Mượn từ các tài liệu Python , ở trên sẽ mở rộng thành một cái gì đó 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

class C:
    def f(cls):
        print(f'f called with cls={cls}')
    f = ClassMethod(f)

Lưu ý cách __get__ có thể lấy một thể hiện hoặc lớp (hoặc cả hai), và do đó bạn có thể làm cả hai C.fC().f. Đây là không giống như ví dụ metaclass bạn đưa ra mà sẽ ném một AttributeErrorcho C().f.

Hơn nữa, trong ví dụ siêu dữ liệu, fkhông tồn tại trong C.__dict__. Khi tra cứu thuộc tính fvới C.f, trình thông dịch nhìn vào C.__dict__và sau đó không tìm thấy, nhìn vào type(C).__dict__(đó là M.__dict__). Điều này có thể quan trọng nếu bạn muốn sự linh hoạt để ghi đè ftrong C, mặc dù tôi nghi ngờ bao giờ này sẽ được sử dụng thực tế.


0

Trong ví dụ của bạn, sự khác biệt sẽ là ở một số lớp khác sẽ có M được đặt làm siêu dữ liệu của chúng.

class M(type):
    def f(cls):
        pass

class C(metaclass=M):
    pass

class C2(metaclass=M):
    pass

C.f()
C2.f()
class M(type):
     pass

class C(metaclass=M):
     @classmethod
     def f(cls):
        pass

class C2(metaclass=M):
    pass

C.f()
# C2 does not have 'f'

Dưới đây là nhiều hơn về metaclass Một số trường hợp sử dụng (cụ thể) cho metaclass là gì?


0

Cả @ classmethod và Metaclass đều khác nhau.

Tất cả mọi thứ trong python là một đối tượng. Mỗi thứ có nghĩa là mọi thứ.

Metaclass là gì?

Như đã nói mọi thứ là một đối tượng. Các lớp cũng là các đối tượng trong các lớp thực tế là các thể hiện của các đối tượng bí ẩn khác được gọi chính thức là các lớp meta. Siêu dữ liệu mặc định trong python là "type" nếu không được chỉ định

Theo mặc định, tất cả các lớp được định nghĩa là các thể hiện của loại.

Các lớp là các thể hiện của Meta-Class

Vài điểm quan trọng là để hiểu hành vi đo lường

  • Vì các lớp là các thể hiện của các lớp meta.
  • Giống như mọi đối tượng được khởi tạo, như các đối tượng (thể hiện) có được các thuộc tính của chúng từ lớp. Class sẽ nhận các thuộc tính của nó từ Meta-Class

Xem xét mã sau

class Meta(type):
    def foo(self):
        print(f'foo is called self={self}')
        print('{} is instance of {}: {}'.format(self, Meta, isinstance(self, Meta)))

class C(metaclass=Meta):
    pass

C.foo()

Ở đâu,

  • lớp C là ví dụ của lớp Meta
  • "class C" là đối tượng lớp là thể hiện của "lớp Meta"
  • Giống như bất kỳ đối tượng nào khác (ví dụ) "lớp C" có quyền truy cập các thuộc tính / phương thức được định nghĩa trong lớp "lớp Meta" của nó
  • Vì vậy, giải mã "C.foo ()". "C" là ví dụ của "Meta" và "foo" là phương thức gọi thông qua thể hiện của "Meta" là "C".
  • Đối số đầu tiên của phương thức "foo" là tham chiếu đến thể hiện không phải là lớp không giống như "classmethod"

Chúng tôi có thể xác minh như thể "lớp C" là ví dụ của "Lớp Meta

  isinstance(C, Meta)

Phân loại là gì?

Các phương thức Python được cho là bị ràng buộc. Vì python áp đặt các hạn chế mà phương thức phải được gọi chỉ với ví dụ. Đôi khi chúng ta có thể muốn gọi các phương thức trực tiếp qua lớp mà không cần bất kỳ trường hợp nào (giống như các thành viên tĩnh trong java) mà không cần phải tạo bất kỳ cá thể nào. Ví dụ mặc định của tôi là bắt buộc để gọi phương thức. Như một python giải pháp cung cấp phân loại hàm tích hợp để liên kết phương thức đã cho với lớp thay vì thể hiện.

Khi các phương thức lớp được ràng buộc với lớp. Phải mất ít nhất một đối số tham chiếu đến chính lớp thay vì cá thể (tự)

nếu chức năng / lớp trang trí tích hợp được sử dụng. Đối số đầu tiên sẽ được tham chiếu đến lớp thay vì ví dụ

class ClassMethodDemo:
    @classmethod
    def foo(cls):
        print(f'cls is ClassMethodDemo: {cls is ClassMethodDemo}')

Như chúng ta đã sử dụng "classmethod", chúng ta gọi phương thức "foo" mà không tạo bất kỳ trường hợp nào như sau

ClassMethodDemo.foo()

Cuộc gọi phương thức trên sẽ trả về True. Vì đối số đầu tiên cls thực sự là tham chiếu đến "ClassMethodDemo"

Tóm lược:

  • Classmethod nhận được đối số đầu tiên là "tham chiếu đến lớp (theo truyền thống được gọi là cls")
  • Các phương thức của các siêu lớp không phải là phân loại. Các phương thức của các lớp Meta nhận được đối số đầu tiên là "tham chiếu đến thể hiện (theo truyền thống là tự) không phải là lớp"
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.