Tại sao việc đặt một bộ mô tả trên một lớp ghi đè lên bộ mô tả?


10

Repro đơn giản:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

class B(object):
    v = VocalDescriptor()

B.v # prints "__get__, obj=None, objtype=<class '__main__.B'>"
B.v = 3 # does not print "__set__", evidently does not trigger descriptor
B.v # does not print anything, we overwrote the descriptor

Câu hỏi này có một bản sao hiệu quả , nhưng bản sao không được trả lời và tôi đã đào sâu thêm một chút vào nguồn CPython như một bài tập học tập. Cảnh báo: tôi đã đi vào đám cỏ dại. Tôi thực sự hy vọng tôi có thể nhận được sự giúp đỡ từ một thuyền trưởng biết những vùng biển đó . Tôi đã cố gắng rõ ràng nhất có thể trong việc truy tìm các cuộc gọi mà tôi đang tìm kiếm, vì lợi ích tương lai của chính tôi và lợi ích của các độc giả tương lai.

Tôi đã thấy rất nhiều mực tràn ra hành vi __getattribute__áp dụng cho các mô tả, ví dụ như ưu tiên tra cứu. Các Python đoạn trích trong "Gọi Phần mô tả" ngay dưới For classes, the machinery is in type.__getattribute__()...khoảng đồng ý bằng tâm trí của tôi với những gì tôi tin là tương ứng với nguồn CPython trong type_getattro, mà tôi theo dõi xuống bằng cách nhìn vào "tp_slots" thì nơi tp_getattro được phổ biến . Và thực tế là B.vbản in ban đầu __get__, obj=None, objtype=<class '__main__.B'>có ý nghĩa với tôi.

Điều tôi không hiểu là tại sao bài tập lại B.v = 3ghi đè một cách mù quáng vào phần mô tả, thay vì kích hoạt v.__set__? Tôi đã cố gắng theo dõi cuộc gọi CPython, bắt đầu lại từ "tp_slots" , sau đó nhìn vào nơi tp_setattro được điền , sau đó nhìn vào type_setattro . type_setattro dường như là một trình bao bọc mỏng xung quanh _PyObject_GenericSetAttrWithDict . Và có mấu chốt của sự nhầm lẫn của tôi: _PyObject_GenericSetAttrWithDictdường như có logic ưu tiên cho __set__phương pháp của một người mô tả !! Với suy nghĩ này, tôi không thể hiểu tại sao lại B.v = 3ghi đè một cách mù quáng vthay vì kích hoạt v.__set__.

Tuyên bố miễn trừ trách nhiệm 1: Tôi đã không xây dựng lại Python từ nguồn bằng các bản in, vì vậy tôi không hoàn toàn chắc chắn type_setattrolà những gì được gọi trong thời gian này B.v = 3.

Tuyên bố miễn trừ trách nhiệm 2: VocalDescriptorkhông nhằm mục đích minh họa cho định nghĩa mô tả "điển hình" hoặc "khuyến nghị". Đó là một no-op dài dòng để cho tôi biết khi nào các phương thức đang được gọi.


1
Đối với tôi, bản in 3 này ở dòng cuối cùng ... Mã hoạt động tốt
Jab

3
Các mô tả áp dụng khi truy cập các thuộc tính từ một thể hiện , không phải chính lớp đó. Đối với tôi, bí ẩn là tại sao __get__làm việc cả, hơn là tại sao __set__không.
jasonharper

1
@Jab OP dự kiến ​​vẫn gọi __get__phương thức. B.v = 3đã ghi đè lên thuộc tính một cách hiệu quả với một int.
r.ook

2
@jasonharper Truy cập thuộc tính xác định xem có __get__được gọi hay không, và các cài đặt mặc định object.__getattribute__type.__getattribute__gọi __get__khi sử dụng một thể hiện hoặc lớp. Chỉ định thông qua __set__là chỉ thể hiện.
chepner

@jasonharper Tôi tin rằng __get__các phương thức của mô tả được cho là kích hoạt khi được gọi từ chính lớp đó. Đây là cách @ classmethods và @staticmethods được thực hiện, theo hướng dẫn cách làm . @Jab Tôi tự hỏi tại sao B.v = 3có thể ghi đè lên mô tả lớp. Dựa trên việc thực hiện CPython, tôi dự kiến B.v = 3cũng sẽ kích hoạt __set__.
Michael Carilli

Câu trả lời:


6

Bạn đúng là B.v = 3chỉ đơn giản ghi đè lên bộ mô tả bằng một số nguyên (nếu cần).

Để B.v = 3gọi một bộ mô tả, bộ mô tả nên được xác định trên siêu dữ liệu, tức là trên type(B).

>>> class BMeta(type): 
...     v = VocalDescriptor() 
... 
>>> class B(metaclass=BMeta): 
...     pass 
... 
>>> B.v = 3 
__set__

Để gọi mô tả trên B, bạn sẽ sử dụng một thể hiện: B().v = 3sẽ thực hiện nó.

Lý do để B.vgọi getter là để cho phép trả về chính cá thể mô tả. Thông thường bạn sẽ làm điều đó, để cho phép truy cập vào bộ mô tả thông qua đối tượng lớp:

class VocalDescriptor(object):
    def __get__(self, obj, objtype):
        if obj is None:
            return self
        print('__get__, obj={}, objtype={}'.format(obj, objtype))
    def __set__(self, obj, val):
        print('__set__')

Bây giờ B.vsẽ trả về một số trường hợp như <mymodule.VocalDescriptor object at 0xdeadbeef>bạn có thể tương tác. Nó thực sự là đối tượng mô tả, được định nghĩa là một thuộc tính lớp và trạng thái của nó B.v.__dict__được chia sẻ giữa tất cả các phiên bản của B.

Tất nhiên, tùy thuộc vào mã của người dùng để xác định chính xác những gì họ muốn B.vlàm, trả lại selfchỉ là mô hình phổ biến.


1
Để hoàn thành câu trả lời này, tôi sẽ thêm nó __get__được thiết kế để được gọi là thuộc tính thể hiện hoặc thuộc tính lớp, nhưng __set__được thiết kế để chỉ được gọi là thuộc tính thể hiện. Và tài liệu liên quan: docs.python.org/3/reference/datamodel.html#object.__get__
sanyash

@wim Tuyệt vời !! Song song, tôi đã tìm kiếm một lần nữa ở chuỗi cuộc gọi type_setattro. Tôi thấy rằng lệnh gọi _PyObject_GenericSetAttrWithDict cung cấp loại (tại thời điểm đó B, trong trường hợp của tôi).
Michael Carilli

Trong _PyObject_GenericSetAttrWithDict, nó kéo Py_TYPE của B như tp, đó là metaclass B ( typetrong trường hợp của tôi), sau đó nó là metaclass tp đó được xử lý bởi các mô tả Logic ngắn mạch . Vì vậy, bộ mô tả được định nghĩa trực tiếp B không được nhìn thấy bởi logic ngắn mạch đó (do đó trong mã gốc của tôi __set__không được gọi), nhưng một bộ mô tả được xác định trên siêu dữ liệu được nhìn thấy bằng logic ngắn mạch.
Michael Carilli

Do đó, trong trường hợp của bạn khi siêu dữ liệu có một bộ mô tả, __set__phương thức của bộ mô tả đó được gọi.
Michael Carilli

@sanyash cứ thoải mái chỉnh sửa trực tiếp.
wim

3

Chặn bất kỳ phần ghi đè nào, B.vtương đương với type.__getattribute__(B, "v"), trong khi b = B(); b.vtương đương với object.__getattribute__(b, "v"). Cả hai định nghĩa đều gọi __get__phương thức của kết quả nếu được định nghĩa.

Lưu ý, suy nghĩ, rằng cuộc gọi để __get__khác nhau trong từng trường hợp. B.vvượt qua Nonenhư đối số đầu tiên, trong khi B().vvượt qua chính thể hiện. Trong cả hai trường hợp Bđược thông qua như là đối số thứ hai.

B.v = 3mặt khác, tương đương với type.__setattr__(B, "v", 3), không gọi __set__.

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.