Hướng dẫn mô tả

tác giả:

Raymond Hettinger

Liên hệ:

<python tại rcn dot com>

Descriptors cho phép các đối tượng tùy chỉnh việc tra cứu, lưu trữ và xóa thuộc tính.

Hướng dẫn này có bốn phần chính:

  1. Phần “mồi” đưa ra một cái nhìn tổng quan cơ bản, di chuyển nhẹ nhàng từ các ví dụ đơn giản, thêm từng tính năng một. Bắt đầu ở đây nếu bạn chưa quen với bộ mô tả.

  2. Phần thứ hai trình bày một ví dụ mô tả thực tế, đầy đủ. Nếu bạn đã biết những điều cơ bản, hãy bắt đầu từ đó.

  3. Phần thứ ba cung cấp hướng dẫn kỹ thuật hơn đi sâu vào cơ chế chi tiết về cách hoạt động của bộ mô tả. Hầu hết mọi người không cần mức độ chi tiết này.

  4. Phần cuối cùng có các phần tương đương Python thuần túy cho các bộ mô tả tích hợp được viết bằng C. Hãy đọc phần này nếu bạn tò mò về cách các hàm biến thành các phương thức ràng buộc hoặc về cách triển khai các công cụ phổ biến như classmethod(), staticmethod(), property()__slots__.

Sơn lót

Trong phần mở đầu này, chúng ta bắt đầu với ví dụ cơ bản nhất có thể và sau đó chúng ta sẽ thêm từng khả năng mới.

Ví dụ đơn giản: Một bộ mô tả trả về một hằng số

Lớp Ten là một bộ mô tả có phương thức __get__() luôn trả về hằng số 10:

lớp mười:
    def __get__(self, obj, objtype=None):
        trở lại 10

Để sử dụng bộ mô tả, nó phải được lưu trữ dưới dạng biến lớp trong lớp khác:

lớp A:
    x = 5 thuộc tính lớp # Regular
    y = Ten() # Descriptor dụ

Phiên tương tác cho thấy sự khác biệt giữa tra cứu thuộc tính thông thường và tra cứu mô tả:

>>> a = A() # Make một thể hiện của lớp A
>>> tra cứu thuộc tính a.x # Normal
5
>>> tra cứu a.y # Descriptor
10

Trong tra cứu thuộc tính a.x, toán tử dấu chấm tìm thấy 'x': 5 trong từ điển lớp. Trong tra cứu a.y, toán tử dấu chấm tìm thấy một phiên bản mô tả, được nhận dạng bằng phương thức __get__ của nó. Gọi phương thức đó trả về 10.

Lưu ý rằng giá trị 10 không được lưu trữ trong từ điển lớp hoặc từ điển cá thể. Thay vào đó, giá trị 10 được tính theo yêu cầu.

Ví dụ này cho thấy cách hoạt động của một bộ mô tả đơn giản nhưng nó không hữu ích lắm. Để truy xuất các hằng số, tra cứu thuộc tính thông thường sẽ tốt hơn.

Trong phần tiếp theo, chúng ta sẽ tạo một thứ hữu ích hơn, tra cứu động.

Tra cứu động

Các bộ mô tả thú vị thường chạy tính toán thay vì trả về các hằng số:

hệ điều hành nhập khẩu

Kích thước thư mục lớp:

    def __get__(self, obj, objtype=None):
        trả về len(os.listdir(obj.dirname))

Thư mục lớp:

    size =  dụ DirectorySize() # Descriptor

    def __init__(self, dirname):
        self.dirname = thuộc tính dirname # Regular

Một phiên tương tác cho thấy việc tra cứu diễn ra linh hoạt — nó tính toán các câu trả lời được cập nhật, khác nhau mỗi lần:

>>> s = Thư mục('bài hát')
>>> g = Thư mục('trò chơi')
>>> thư mục bài hát s.size # The có hai mươi tập tin
20
>>> Thư mục trò chơi g.size # The có ba tệp
3
>>> os.remove('games/chess') # Delete một trò chơi
>>> số lượng g.size # File được cập nhật tự động
2

Bên cạnh việc hiển thị cách các bộ mô tả có thể chạy tính toán, ví dụ này cũng cho thấy mục đích của các tham số đối với __get__(). Tham số selfsize, một phiên bản của DirectorySize. Tham số objg hoặc s, một phiên bản của Directory. Tham số obj cho phép phương thức __get__() tìm hiểu thư mục đích. Tham số objtype là lớp Directory.

Thuộc tính được quản lý

Cách sử dụng phổ biến của bộ mô tả là quản lý quyền truy cập vào dữ liệu phiên bản. Bộ mô tả được gán cho thuộc tính công khai trong từ điển lớp trong khi dữ liệu thực tế được lưu trữ dưới dạng thuộc tính riêng tư trong từ điển mẫu. Các phương thức __get__()__set__() của bộ mô tả được kích hoạt khi thuộc tính public được truy cập.

Trong ví dụ sau, age là thuộc tính công khai và _age là thuộc tính riêng tư. Khi thuộc tính công khai được truy cập, bộ mô tả sẽ ghi lại việc tra cứu hoặc cập nhật:

nhập nhật 

logging.basicConfig(level=logging.INFO)

lớp LoggedAgeAccess:

    def __get__(self, obj, objtype=None):
        giá trị = obj._age
        logging.info('Truy cập %r cho %r', 'age', value)
        giá trị trả về

    def __set__(self, obj, value):
        logging.info('Đang cập nhật %r lên %r', 'age', value)
        obj._age = giá trị

lớp người:

    age = Phiên bản LoggedAgeAccess() # Descriptor

    def __init__(bản thân, tên, tuổi):
        self.name = tên thuộc tính  thể # Regular
        self.age = tuổi # Calls __set__()

    ngày sinh chắc chắn (bản thân):
        self.age += 1 # Calls cả __get__() và __set__()

Một phiên tương tác cho thấy rằng tất cả quyền truy cập vào thuộc tính được quản lý age đều được ghi lại, nhưng thuộc tính thông thường name không được ghi lại:

>>> mary = Person('Mary M', 30) # The cập nhật tuổi ban đầu được ghi lại
INFO:root:Đang cập nhật 'tuổi' lên 30
>>> dave = Người('David D', 40)
INFO:root:Đang cập nhật 'tuổi' lên 40

>>> dữ liệu thực tế của vars(mary) # The nằm trong thuộc tính riêng tư
{'name': 'Mary M', '_age': 30}
>>> vars(dave)
{'name': 'David D', '_age': 40}

>>> mary.age # Access dữ liệu và nhật ký tra cứu
INFO:root:Truy cập 'tuổi' cho 30
30
>>> mary.birthday() # Updates cũng được ghi lại
INFO:root:Truy cập 'tuổi' cho 30
INFO:root:Đang cập nhật 'tuổi' lên 31

>>> tra cứu thuộc tính dave.name # Regular không được ghi lại
'David D'
>>> dave.age # Only thuộc tính được quản lý đã được ghi lại
INFO:root:Truy cập 'tuổi' cho 40
40

Một vấn đề lớn với ví dụ này là tên riêng _age được gắn chặt trong lớp LoggedAgeAccess. Điều đó có nghĩa là mỗi phiên bản chỉ có thể có một thuộc tính được ghi và tên của nó không thể thay đổi. Trong ví dụ tiếp theo, chúng tôi sẽ khắc phục vấn đề đó.

Tên tùy chỉnh

Khi một lớp sử dụng các bộ mô tả, nó có thể thông báo cho mỗi bộ mô tả về tên biến nào đã được sử dụng.

Trong ví dụ này, lớp Person có hai phiên bản mô tả, nameage. Khi lớp Person được xác định, nó sẽ gọi lại __set_name__() trong LoggedAccess để có thể ghi lại tên trường, cung cấp cho mỗi bộ mô tả public_nameprivate_name của riêng nó:

nhập nhật 

logging.basicConfig(level=logging.INFO)

lớp LoggedAccess:

    def __set_name__(bản thân, chủ sở hữu, tên):
        self.public_name = tên
        self.private_name = '_' + tên

    def __get__(self, obj, objtype=None):
        value = getattr(obj, self.private_name)
        logging.info('Truy cập %r cho %r', self.public_name, value)
        giá trị trả về

    def __set__(self, obj, value):
        logging.info('Đang cập nhật %r lên %r', self.public_name, value)
        setattr(obj, self.private_name, value)

lớp người:

    name = Phiên bản  tả LoggedAccess() # First
    age = Phiên bản  tả LoggedAccess() # Second

    def __init__(bản thân, tên, tuổi):
        self.name = tên # Calls mô tả đầu tiên
        self.age = age # Calls mô tả thứ hai

    ngày sinh chắc chắn (bản thân):
        bản thân.tuổi += 1

Một phiên tương tác cho thấy lớp Person đã gọi __set_name__() để ghi lại tên trường. Ở đây chúng tôi gọi vars() để tra cứu bộ mô tả mà không kích hoạt nó:

>>> vars(vars(Person)['name'])
{'public_name': 'name', 'private_name': '_name'}
>>> vars(vars(Person)['age'])
{'public_name': 'age', 'private_name': '_age'}

Lớp mới hiện ghi lại quyền truy cập vào cả nameage:

>>> pete = Người('Peter P', 10)
INFO:root:Đang cập nhật 'tên' thành 'Peter P'
INFO:root:Đang cập nhật 'tuổi' lên 10
>>> kate = Person('Catherine C', 20)
INFO:root:Đang cập nhật 'tên' thành 'Catherine C'
INFO:root:Đang cập nhật 'tuổi' lên 20

Hai phiên bản Person chỉ chứa tên riêng:

>>> vars(pete)
{'_name': 'Peter P', '_age': 10}
>>> vars(kate)
{'_name': 'Catherine C', '_age': 20}

Kết thúc suy nghĩ

descriptor là tên mà chúng tôi gọi bất kỳ đối tượng nào xác định __get__(), __set__() hoặc __delete__().

Tùy chọn, bộ mô tả có thể có phương thức __set_name__(). Điều này chỉ được sử dụng trong trường hợp bộ mô tả cần biết lớp nơi nó được tạo hoặc tên của biến lớp mà nó được gán. (Phương thức này, nếu có, sẽ được gọi ngay cả khi lớp đó không phải là bộ mô tả.)

Bộ mô tả được toán tử dấu chấm gọi ra trong quá trình tra cứu thuộc tính. Nếu một bộ mô tả được truy cập gián tiếp bằng vars(some_class)[descriptor_name], thì thể hiện của bộ mô tả sẽ được trả về mà không gọi nó.

Bộ mô tả chỉ hoạt động khi được sử dụng làm biến lớp. Khi đưa vào instance, chúng không có tác dụng.

Động lực chính của bộ mô tả là cung cấp một cái móc cho phép các đối tượng được lưu trữ trong các biến lớp kiểm soát những gì xảy ra trong quá trình tra cứu thuộc tính.

Theo truyền thống, lớp gọi sẽ kiểm soát những gì xảy ra trong quá trình tra cứu. Bộ mô tả đảo ngược mối quan hệ đó và cho phép dữ liệu được tra cứu có tiếng nói trong vấn đề.

Mô tả được sử dụng trong suốt ngôn ngữ. Đó là cách các hàm biến thành các phương thức bị ràng buộc. Các công cụ phổ biến như classmethod(), staticmethod(), property()functools.cached_property() đều được triển khai dưới dạng mô tả.

Hoàn thành ví dụ thực tế

Trong ví dụ này, chúng tôi tạo ra một công cụ thiết thực và mạnh mẽ để xác định các lỗi hỏng dữ liệu nổi tiếng là khó tìm.

Lớp xác thực

Trình xác thực là một bộ mô tả để truy cập thuộc tính được quản lý. Trước khi lưu trữ bất kỳ dữ liệu nào, nó sẽ xác minh rằng giá trị mới đáp ứng các hạn chế về loại và phạm vi khác nhau. Nếu những hạn chế đó không được đáp ứng, nó sẽ đưa ra một ngoại lệ để ngăn chặn tình trạng hỏng dữ liệu tại nguồn.

Lớp Validator này vừa là abstract base class vừa là bộ mô tả thuộc tính được quản lý:

từ abc nhập ABC, phương pháp trừu tượng

Trình xác thực lớp (ABC):

    def __set_name__(bản thân, chủ sở hữu, tên):
        self.private_name = '_' + tên

    def __get__(self, obj, objtype=None):
        trả về getattr(obj, self.private_name)

    def __set__(self, obj, value):
        self.validate(value)
        setattr(obj, self.private_name, value)

    @abstractmethod
    xác thực xác thực (tự, giá trị):
        vượt qua

Trình xác thực tùy chỉnh cần kế thừa từ Validator và phải cung cấp phương thức validate() để kiểm tra các hạn chế khác nhau nếu cần.

Trình xác thực tùy chỉnh

Dưới đây là ba tiện ích xác thực dữ liệu thực tế:

  1. OneOf xác minh rằng một giá trị là một trong các tùy chọn bị hạn chế.

  2. Number xác minh rằng giá trị là int hoặc float. Tùy chọn, nó xác minh rằng một giá trị nằm trong khoảng tối thiểu hoặc tối đa nhất định.

  3. String xác minh rằng giá trị là str. Tùy chọn, nó xác nhận độ dài tối thiểu hoặc tối đa nhất định. Nó cũng có thể xác nhận predicate do người dùng xác định.

lớp OneOf(Trình xác thực):

    def __init__(tự, *tùy chọn):
        self.options = set(options)

    xác thực xác thực (tự, giá trị):
        nếu giá trị không  trong self.options:
            tăng giá trịError(
                f'Dự kiến {value!r} sẽ là một trong {self.options!r}'
            )

Số lớp (Trình xác thực):

    def __init__(self, minvalue=None, maxvalue=None):
        self.minvalue = giá trị tối thiểu
        self.maxvalue = giá trị tối đa

    xác thực xác thực (tự, giá trị):
        nếu không phải  isinstance(value, (int, float)):
            raise TypeError(f'Dự kiến {value!r} là int hoặc float')
        nếu self.minvalue không phải  Không  giá trị < self.minvalue:
            tăng giá trịError(
                f'Dự kiến {value!r} ít nhất phải là {self.minvalue!r}'
            )
        nếu self.maxvalue không phải  Không   giá trị > self.maxvalue:
            tăng giá trịError(
                f'Dự kiến {value!r} không lớn hơn {self.maxvalue!r}'
            )

Chuỗi lớp (Trình xác thực):

    def __init__(self, minsize=None, maxsize=None, predicate=None):
        self.minsize = kích thước tối thiểu
        self.maxsize = kích thước tối đa
        self.predicate = vị ngữ

    xác thực xác thực (tự, giá trị):
        nếu không phải  isinstance(value, str):
            raise TypeError(f'Dự kiến {value!r} là một str')
        nếu self.minsize không phải  Không  len(value) < self.minsize:
            tăng giá trịError(
                f'Dự kiến {value!r} không nhỏ hơn {self.minsize!r}'
            )
        nếu self.maxsize không phải  Không  len(value) > self.maxsize:
            tăng giá trịError(
                f'Dự kiến {value!r} không lớn hơn {self.maxsize!r}'
            )
        nếu self.predicate không phải  None  không phải self.predicate(value):
            tăng giá trịError(
                f'Dự kiến {self.predicate} là đúng cho {value!r}'
            )

Ứng dụng thực tế

Đây là cách trình xác thực dữ liệu có thể được sử dụng trong một lớp thực:

Thành phần lớp:

    name = String(minsize=3, maxsize=10, predicate=str.isupper)
    kind = OneOf('gỗ', 'kim loại', 'nhựa')
    số lượng = Số (giá trị tối thiểu = 0)

    def __init__(bản thân, tên, loại, số lượng):
        self.name = tên
        self.kind = tử tế
        self.quantity = số lượng

Các bộ mô tả ngăn chặn việc tạo các trường hợp không hợp lệ:

>>> Component('Widget', 'metal', 5) # Blocked: 'Widget' không phải toàn chữ hoa
Traceback (cuộc gọi gần đây nhất):
    ...
ValueError: <phương thức 'isupper' của đối tượng 'str' được mong đợi là đúng với 'Widget'

>>> Component('WIDGET', 'metle', 5) # Blocked: 'metle' sai chính tả
Traceback (cuộc gọi gần đây nhất):
    ...
ValueError: 'metle' được mong đợi là một trong các {'kim loại', 'nhựa', 'gỗ'}

>>> Thành phần('WIDGET', 'metal', -5) # Blocked: -5 là âm
Traceback (cuộc gọi gần đây nhất):
    ...
ValueError: Dự kiến ​​-5 ít nhất là 0

>>> Component('WIDGET', 'metal', 'V') # Blocked: 'V' không phải là số
Traceback (cuộc gọi gần đây nhất):
    ...
TypeError: Dự kiến 'V' là int hoặc float

>>> c = Thành phần('WIDGET', 'metal', 5) # Allowed: Dữ liệu đầu vào hợp lệ

Hướng dẫn kỹ thuật

Phần tiếp theo là hướng dẫn kỹ thuật hơn về cơ chế và chi tiết về cách hoạt động của bộ mô tả.

Tóm tắt

Xác định các bộ mô tả, tóm tắt giao thức và hiển thị cách gọi các bộ mô tả. Cung cấp một ví dụ cho thấy cách hoạt động của ánh xạ quan hệ đối tượng.

Tìm hiểu về các bộ mô tả không chỉ cung cấp quyền truy cập vào bộ công cụ lớn hơn mà còn tạo ra sự hiểu biết sâu sắc hơn về cách hoạt động của Python.

Định nghĩa và giới thiệu

Nói chung, bộ mô tả là một giá trị thuộc tính có một trong các phương thức trong giao thức bộ mô tả. Những phương pháp đó là __get__(), __set__()__delete__(). Nếu bất kỳ phương thức nào trong số đó được xác định cho một thuộc tính thì nó được gọi là descriptor.

Hành vi mặc định để truy cập thuộc tính là lấy, đặt hoặc xóa thuộc tính khỏi từ điển của đối tượng. Ví dụ: a.x có chuỗi tra cứu bắt đầu bằng a.__dict__['x'], sau đó là type(a).__dict__['x'] và tiếp tục thông qua thứ tự phân giải phương thức của type(a). Nếu giá trị tra cứu là một đối tượng xác định một trong các phương thức mô tả thì Python có thể ghi đè hành vi mặc định và thay vào đó gọi phương thức mô tả. Trường hợp điều này xảy ra trong chuỗi ưu tiên phụ thuộc vào phương thức mô tả nào được xác định.

Bộ mô tả là một giao thức có mục đích chung, mạnh mẽ. Chúng là cơ chế đằng sau các thuộc tính, phương thức, phương thức tĩnh, phương thức lớp và super(). Chúng được sử dụng xuyên suốt Python. Bộ mô tả đơn giản hóa mã C cơ bản và cung cấp một bộ công cụ mới linh hoạt cho các chương trình Python hàng ngày.

Giao thức mô tả

descr.__get__(self, obj, type=None)

descr.__set__(self, obj, value)

descr.__delete__(self, obj)

Đó là tất cả những gì cần có. Xác định bất kỳ phương thức nào trong số này và một đối tượng được coi là bộ mô tả và có thể ghi đè hành vi mặc định khi được tra cứu dưới dạng thuộc tính.

Nếu một đối tượng xác định __set__() hoặc __delete__(), thì nó được coi là bộ mô tả dữ liệu. Các bộ mô tả chỉ xác định __get__() được gọi là bộ mô tả phi dữ liệu (chúng thường được sử dụng cho các phương thức nhưng cũng có thể sử dụng cho các mục đích khác).

Bộ mô tả dữ liệu và phi dữ liệu khác nhau về cách tính toán phần ghi đè đối với các mục trong từ điển của một phiên bản. Nếu từ điển của một phiên bản có mục nhập có cùng tên với bộ mô tả dữ liệu thì bộ mô tả dữ liệu sẽ được ưu tiên. Nếu từ điển của một phiên bản có mục nhập có cùng tên với phần mô tả không phải dữ liệu thì mục nhập từ điển đó sẽ được ưu tiên.

Để tạo bộ mô tả dữ liệu chỉ đọc, hãy xác định cả __get__()__set__() với __set__() tăng AttributeError khi được gọi. Việc xác định phương thức __set__() bằng một trình giữ chỗ nâng cao ngoại lệ là đủ để biến nó thành một bộ mô tả dữ liệu.

Tổng quan về lời gọi mô tả

Một bộ mô tả có thể được gọi trực tiếp bằng desc.__get__(obj) hoặc desc.__get__(None, cls).

Nhưng thông thường hơn là một bộ mô tả sẽ được gọi tự động khi truy cập thuộc tính.

Biểu thức obj.x tra cứu thuộc tính x trong chuỗi không gian tên cho obj. Nếu tìm kiếm tìm thấy một bộ mô tả bên ngoài cá thể __dict__, thì phương thức __get__() của nó sẽ được gọi theo các quy tắc ưu tiên được liệt kê bên dưới.

Chi tiết về lệnh gọi phụ thuộc vào việc obj là một đối tượng, lớp hay phiên bản của super.

Lời gọi từ một instance

Tra cứu phiên bản quét qua một chuỗi các không gian tên cung cấp cho bộ mô tả dữ liệu mức độ ưu tiên cao nhất, tiếp theo là các biến phiên bản, sau đó là các bộ mô tả không phải dữ liệu, sau đó là các biến lớp và cuối cùng là __getattr__() nếu được cung cấp.

Nếu tìm thấy bộ mô tả cho a.x thì nó sẽ được gọi bằng: desc.__get__(a, type(a)).

Logic cho tra cứu theo dấu chấm là object.__getattribute__(). Đây là một Python thuần túy tương đương:

def find_name_in_mro(cls, tên, mặc định):
    "Giả lập _PyType_Lookup() trong Objects/typeobject.c"
    cho  sở trong cls.__mro__:
        nếu tên trong vars ( sở):
            trả về vars(base)[name]
    trả về mặc định

def object_getattribute(obj, name):
    "Giả lập PyObject_GenericGetAttr() trong Objects/object.c"
    null = đối tượng()
    objtype = loại(obj)
    cls_var = find_name_in_mro(objtype, tên, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    nếu descr_get không rỗng:
        if (hasattr(type(cls_var), '__set__')
            hoặc hasattr(type(cls_var), '__delete__')):
            trả về descr_get(cls_var, obj, objtype) bộ  tả # data
    nếu hasattr(obj, '__dict__')  tên trong vars(obj):
        trả về biến vars(obj)[name] # instance
    nếu descr_get không rỗng:
        trả về descr_get(cls_var, obj, objtype) bộ  tả # non-data
    nếu cls_var không rỗng:
        trả về biến cls_var # class
    tăng AttributionError(name)

Lưu ý, không có hook __getattr__() trong mã __getattribute__(). Đó là lý do tại sao gọi __getattribute__() trực tiếp hoặc bằng super().__getattribute__ sẽ bỏ qua __getattr__() hoàn toàn.

Thay vào đó, toán tử dấu chấm và hàm getattr() chịu trách nhiệm gọi __getattr__() bất cứ khi nào __getattribute__() tăng AttributeError. Logic của chúng được gói gọn trong một hàm trợ giúp:

def getattr_hook(obj, tên):
    "Giả lập slot_tp_getattr_hook() trong Objects/typeobject.c"
    thử:
        trả về obj.__getattribute__(name)
    ngoại trừ AttributionError:
        nếu không  hasattr(type(obj), '__getattr__'):
            nâng cao
    kiểu trả về(obj).__getattr__(obj, name) # __getattr__

Lời gọi từ một lớp

Logic cho tra cứu theo dấu chấm chẳng hạn như A.x nằm trong type.__getattribute__(). Các bước này tương tự như các bước dành cho object.__getattribute__() nhưng việc tra cứu từ điển phiên bản được thay thế bằng tìm kiếm thông qua method resolution order của lớp.

Nếu tìm thấy một bộ mô tả, nó sẽ được gọi bằng desc.__get__(None, A).

Việc triển khai C đầy đủ có thể được tìm thấy trong type_getattro()_PyType_Lookup() trong Objects/typeobject.c.

Lời gọi từ siêu

Logic cho việc tra cứu theo dấu chấm của super nằm trong phương thức __getattribute__() cho đối tượng được trả về bởi super().

Tra cứu theo dấu chấm chẳng hạn như super(A, obj).m tìm kiếm obj.__class__.__mro__ cho lớp cơ sở B ngay sau A và sau đó trả về B.__dict__['m'].__get__(obj, A). Nếu không phải là bộ mô tả, m sẽ được trả về không thay đổi.

Việc triển khai C đầy đủ có thể được tìm thấy trong super_getattro() trong Objects/typeobject.c. Có thể tìm thấy một Python thuần túy tương đương trong Guido's Tutorial.

Tóm tắt logic gọi

Cơ chế mô tả được nhúng trong các phương thức __getattribute__() cho object, typesuper().

Những điểm quan trọng cần nhớ là:

  • Bộ mô tả được gọi bằng phương thức __getattribute__().

  • Các lớp kế thừa máy móc này từ object, type hoặc super().

  • Việc ghi đè __getattribute__() sẽ ngăn các lệnh gọi bộ mô tả tự động vì tất cả logic của bộ mô tả đều nằm trong phương thức đó.

  • object.__getattribute__()type.__getattribute__() thực hiện các lệnh gọi khác nhau tới __get__(). Cái đầu tiên bao gồm thể hiện và có thể bao gồm lớp. Cái thứ hai đặt None làm ví dụ và luôn bao gồm lớp.

  • Bộ mô tả dữ liệu luôn ghi đè từ điển phiên bản.

  • Các bộ mô tả phi dữ liệu có thể bị ghi đè bởi các từ điển phiên bản.

Thông báo tên tự động

Đôi khi bộ mô tả muốn biết tên biến lớp mà nó được gán cho. Khi một lớp mới được tạo, siêu dữ liệu type sẽ quét từ điển của lớp mới. Nếu bất kỳ mục nào là mô tả và nếu chúng xác định __set_name__(), thì phương thức đó sẽ được gọi với hai đối số. owner là lớp sử dụng bộ mô tả và name là biến lớp mà bộ mô tả được gán cho.

Chi tiết triển khai có trong type_new()set_names() trong Objects/typeobject.c.

Vì logic cập nhật nằm trong type.__new__() nên thông báo chỉ diễn ra tại thời điểm tạo lớp. Nếu các bộ mô tả được thêm vào lớp sau đó, __set_name__() sẽ cần được gọi theo cách thủ công.

ví dụ về ORM

Đoạn mã sau đây là một khung đơn giản cho thấy cách sử dụng bộ mô tả dữ liệu để triển khai object relational mapping.

Ý tưởng thiết yếu là dữ liệu được lưu trữ trong cơ sở dữ liệu bên ngoài. Các phiên bản Python chỉ giữ các khóa cho các bảng của cơ sở dữ liệu. Bộ mô tả đảm nhiệm việc tra cứu hoặc cập nhật:

Trường lớp:

    def __set_name__(bản thân, chủ sở hữu, tên):
        self.fetch = f'SELECT {name} FROM {owner.table} WHERE {owner.key}=?;'
        self.store = f'UPDATE {owner.table} SET {name}=? WHERE {owner.key}=?;'

    def __get__(self, obj, objtype=None):
        return conn.execute(self.fetch, [obj.key]).fetchone()[0]

    def __set__(self, obj, value):
        conn.execute(self.store, [value, obj.key])
        conn.commit()

Chúng ta có thể sử dụng lớp Field để định nghĩa models mô tả lược đồ cho từng bảng trong cơ sở dữ liệu:

phim lớp:
    table = 'Phim' tên # Table
    phím = 'tiêu đề' phím # Primary
    giám đốc = Field()
    năm = Trường()

    def __init__(tự, khóa):
        self.key = chìa khóa

Bài hát của lớp:
    bảng = 'Âm nhạc'
    khóa = 'tiêu đề'
    nghệ  = Field()
    năm = Trường()
    thể loại = Trường()

    def __init__(tự, khóa):
        self.key = chìa khóa

Để sử dụng các mô hình, trước tiên hãy kết nối với cơ sở dữ liệu:

>>> nhập sqlite3
>>> conn = sqlite3.connect('entertainment.db')

Phiên tương tác cho biết cách lấy dữ liệu từ cơ sở dữ liệu và cách cập nhật dữ liệu:

>>> Phim('Chiến tranh giữa các vì sao').đạo diễn
'George Lucas'
>>> hàm = Phim('Hàm')
>>> f'Được phát hành vào {jaws.year} bởi {jaws.director}'
'Được phát hành năm 1975 bởi Steven Spielberg'

>>> Bài hát('Những con đường quê').nghệ 
'John Denver'

>>> Phim('Chiến tranh giữa các vì sao').director = 'J.J. Abrams'
>>> Phim('Chiến tranh giữa các vì sao').đạo diễn
'J.J. Abrams'

Tương đương Python thuần túy

Giao thức mô tả rất đơn giản và mang lại những khả năng thú vị. Một số trường hợp sử dụng phổ biến đến mức chúng đã được đóng gói sẵn thành các công cụ tích hợp sẵn. Các thuộc tính, phương thức ràng buộc, phương thức tĩnh, phương thức lớp và __slots__ đều dựa trên giao thức mô tả.

Thuộc tính

Gọi property() là một cách ngắn gọn để xây dựng bộ mô tả dữ liệu kích hoạt lệnh gọi hàm khi truy cập vào một thuộc tính. Chữ ký của nó là:

property(fget=None, fset=None, fdel=None, doc=None) -> thuộc tính

Tài liệu cho thấy cách sử dụng điển hình để xác định thuộc tính được quản lý x:

lớp C:
    def getx(self): trả về self.__x
    def setx(self, value): self.__x = value
    def delx(self): del self.__x
    x = property(getx, setx, delx, "Tôi là thuộc tính 'x'.")

Để xem cách property() được triển khai theo giao thức mô tả, đây là một Python thuần túy tương đương thực hiện hầu hết các chức năng cốt lõi:

Thuộc tính lớp:
    "Giả lập PyProperty_Type() trong Objects/descroobject.c"

    def __init__(self, fget=None, fset=None, fdel=None, doc=None):
        self.fget = fget
        self.fset = fset
        self.fdel = fdel
        nếu doc  Không  fget không phải  Không:
            doc = fget.__doc__
        self.__doc__ = tài liệu

    def __set_name__(bản thân, chủ sở hữu, tên):
        self.__name__ = tên

    def __get__(self, obj, objtype=None):
        nếu obj  Không:
            tự trở về
        nếu self.fget  Không:
            tăng lỗi thuộc tính
        trả về self.fget(obj)

    def __set__(self, obj, value):
        nếu self.fset  Không :
            tăng lỗi thuộc tính
        self.fset(obj, value)

    def __delete__(self, obj):
        nếu self.fdel  Không :
            tăng lỗi thuộc tính
        self.fdel(obj)

    def getter(self, fget):
        kiểu trả về(self)(fget, self.fset, self.fdel, self.__doc__)

    setter def (tự, fset):
        kiểu trả về(self)(self.fget, fset, self.fdel, self.__doc__)

    xóa def(self, fdel):
        kiểu trả về(self)(self.fget, self.fset, fdel, self.__doc__)

Nội dung property() sẽ trợ giúp bất cứ khi nào giao diện người dùng cấp quyền truy cập thuộc tính và sau đó các thay đổi tiếp theo yêu cầu sự can thiệp của một phương thức.

Ví dụ: một lớp bảng tính có thể cấp quyền truy cập vào một giá trị ô thông qua Cell('b10').value. Những cải tiến tiếp theo của chương trình yêu cầu ô phải được tính toán lại trong mỗi lần truy cập; tuy nhiên, lập trình viên không muốn ảnh hưởng trực tiếp đến mã máy khách hiện có khi truy cập thuộc tính. Giải pháp là bọc quyền truy cập vào thuộc tính value trong bộ mô tả dữ liệu thuộc tính:

lớp tế bào:
    ...

    @property
    giá trị def (tự):
        "Tính toán lại ô trước khi trả về giá trị"
        self.recalc()
        trả về self._value

property() tích hợp sẵn hoặc Property() tương đương của chúng tôi sẽ hoạt động trong ví dụ này.

Chức năng và phương pháp

Các tính năng hướng đối tượng của Python được xây dựng trên môi trường dựa trên hàm. Bằng cách sử dụng các bộ mô tả không phải dữ liệu, cả hai được hợp nhất một cách liền mạch.

Các hàm được lưu trữ trong từ điển lớp sẽ được chuyển thành các phương thức khi được gọi. Các phương thức chỉ khác với các hàm thông thường ở chỗ đối tượng được thêm vào trước các đối số khác. Theo quy ước, phiên bản này được gọi là self nhưng có thể được gọi là this hoặc bất kỳ tên biến nào khác.

Các phương thức có thể được tạo thủ công với types.MethodType gần tương đương với:

Kiểu phương thức lớp:
    "Giả lập PyMethod_Type trong Objects/classobject.c"

    def __init__(self, func, obj):
        self.__func__ = vui
        self.__self__ = obj

    def __call__(self, *args, **kwargs):
        func = tự.__func__
        obj = tự.__bản thân__
        return func(obj, *args, **kwargs)

    def __getattribute__(bản thân, tên):
        "Giả lập phương thức_getset() trong Objects/classobject.c"
        nếu tên == '__doc__':
            tự trả về.__func__.__doc__
        trả về đối tượng.__getattribute__(self, name)

    def __getattr__(bản thân, tên):
        "Giả lập phương thức_getattro() trong Objects/classobject.c"
        trả về getattr(self.__func__, name)

    def __get__(self, obj, objtype=None):
        "Giả lập phương thức_descr_get() trong Objects/classobject.c"
        tự trở về

Để hỗ trợ việc tạo phương thức tự động, các hàm bao gồm phương thức __get__() cho các phương thức liên kết trong quá trình truy cập thuộc tính. Điều này có nghĩa là các hàm là các bộ mô tả phi dữ liệu trả về các phương thức bị ràng buộc trong quá trình tra cứu theo dấu chấm từ một thể hiện. Đây là cách nó hoạt động:

Chức năng lớp:
    ...

    def __get__(self, obj, objtype=None):
        "Mô phỏng func_descr_get() trong Objects/funcobject.c"
        nếu obj  Không:
            tự trở về
        trả về MethodType(self, obj)

Chạy lớp sau trong trình thông dịch cho thấy cách bộ mô tả hàm hoạt động trong thực tế:

lớp D:
    def f(tự):
         tự trở về

lớp D2:
    vượt qua

Hàm này có thuộc tính qualified name để hỗ trợ việc xem xét nội tâm:

>>> D.f.__qualname__
'Df'

Truy cập hàm thông qua từ điển lớp không gọi __get__(). Thay vào đó, nó chỉ trả về đối tượng hàm cơ bản:

>>> D.__dict__['f']
<chức năng D.f tại 0x00C45070>

Truy cập rải rác từ một lớp gọi __get__(), hàm này chỉ trả về hàm cơ bản không thay đổi

>>> Df
<chức năng D.f tại 0x00C45070>

Hành vi thú vị xảy ra trong quá trình truy cập theo dấu chấm từ một phiên bản. Tra cứu theo dấu chấm gọi __get__() trả về một đối tượng phương thức bị ràng buộc:

>>> d = D()
>>> d.f
<phương thức ràng buộc D.f của đối tượng <__main__.D tại 0x00B18C90>>

Bên trong, phương thức liên kết lưu trữ hàm cơ bản và thể hiện bị ràng buộc

>>> d.f.__func__
<chức năng D.f tại 0x00C45070>

>>> d.f.__chính mình__
<__main__.D đối tượng tại 0x00B18C90>

Nếu bạn đã từng thắc mắc self đến từ đâu trong các phương thức thông thường hoặc cls đến từ đâu trong các phương thức lớp, thì chính là nó!

Các loại phương pháp

Các bộ mô tả phi dữ liệu cung cấp một cơ chế đơn giản cho các biến thể trên các mẫu hàm liên kết thông thường thành các phương thức.

Tóm lại, các hàm có phương thức __get__() để chúng có thể được chuyển đổi thành phương thức khi được truy cập dưới dạng thuộc tính. Bộ mô tả phi dữ liệu chuyển đổi lệnh gọi obj.f(*args) thành f(obj, *args). Gọi cls.f(*args) trở thành f(*args).

Biểu đồ này tóm tắt ràng buộc và hai biến thể hữu ích nhất của nó:

Chuyển đổi

Được gọi từ một đối tượng

Được gọi từ một lớp

chức năng

f(obj, *args)

f(*args)

phương pháp tĩnh

f(*args)

f(*args)

phương pháp phân loại

f(loại(obj), *args)

f(cls, *args)

Phương pháp tĩnh

Các phương thức tĩnh trả về hàm cơ bản mà không thay đổi. Gọi c.f hoặc C.f tương đương với việc tra cứu trực tiếp vào object.__getattribute__(c, "f") hoặc object.__getattribute__(C, "f"). Kết quả là, hàm có thể truy cập được giống hệt nhau từ một đối tượng hoặc một lớp.

Ứng cử viên phù hợp cho các phương thức tĩnh là các phương thức không tham chiếu biến self.

Ví dụ: gói thống kê có thể bao gồm một lớp chứa dữ liệu thử nghiệm. Lớp này cung cấp các phương pháp thông thường để tính toán số liệu thống kê trung bình, trung bình, trung vị và các số liệu thống kê mô tả khác phụ thuộc vào dữ liệu. Tuy nhiên, có thể có những chức năng hữu ích có liên quan về mặt khái niệm nhưng không phụ thuộc vào dữ liệu. Ví dụ: erf(x) là quy trình chuyển đổi tiện dụng xuất hiện trong công việc thống kê nhưng không phụ thuộc trực tiếp vào một tập dữ liệu cụ thể. Nó có thể được gọi từ một đối tượng hoặc lớp: s.erf(1.5) --> 0.9332 hoặc Sample.erf(1.5) --> 0.9332.

Vì các phương thức tĩnh trả về hàm cơ bản mà không có thay đổi nào, nên các lệnh gọi ví dụ không thú vị:

lớp E:
    @staticmethod
    định nghĩa f(x):
        trả về x * 10
>>> E.f(3)
30
>>> E().f(3)
30

Sử dụng giao thức mô tả phi dữ liệu, phiên bản Python thuần túy của staticmethod() sẽ trông như thế này:

nhập khẩu funtools

lớp Phương thức tĩnh:
    "Giả lập PyStaticMethod_Type() trong Objects/funcobject.c"

    định nghĩa __init__(tự, f):
        tự.f = f
        functools.update_wrapper(self, f)

    def __get__(self, obj, objtype=None):
        tự trả về.f

    def __call__(self, *args, **kwds):
        trả về self.f(*args, **kwds)

    @property
    def __annotations__(tự):
        trả về self.f.__annotations__

Lệnh gọi functools.update_wrapper() thêm thuộc tính __wrapped__ đề cập đến hàm cơ bản. Ngoài ra, nó còn chuyển tiếp các thuộc tính cần thiết để làm cho trình bao bọc trông giống như hàm được bao bọc, bao gồm __name__, __qualname____doc__.

Phương thức lớp

Không giống như các phương thức tĩnh, các phương thức lớp thêm tham chiếu lớp vào danh sách đối số trước khi gọi hàm. Định dạng này giống nhau cho dù người gọi là đối tượng hay lớp:

lớp F:
    @classmethod
    def f(cls, x):
        trả về cls.__name__, x
>>> F.f(3)
('F', 3)
>>> F().f(3)
('F', 3)

Hành vi này hữu ích bất cứ khi nào phương thức chỉ cần có tham chiếu lớp và không dựa vào dữ liệu được lưu trữ trong một phiên bản cụ thể. Một cách sử dụng các phương thức lớp là tạo các hàm tạo lớp thay thế. Ví dụ: phương thức lớp dict.fromkeys() tạo một từ điển mới từ danh sách các khóa. Tương đương Python thuần túy là:

lớp Dict(dict):
    @classmethod
    def fromkeys(cls, iterable, value=None):
        "Giả lập dict_fromkeys() trong Objects/dictobject.c"
        d = cls()
        cho khóa trong lần lặp:
            d[khóa] = giá trị
        trả lại d

Bây giờ một từ điển mới gồm các khóa duy nhất có thể được xây dựng như thế này:

>>> d = Dict.fromkeys('abracadabra')
>>> loại (d)  Dict
đúng
>>> d
{'a': Không, 'b': Không, 'r': Không, 'c': Không, 'd': Không}

Sử dụng giao thức mô tả phi dữ liệu, phiên bản Python thuần túy của classmethod() sẽ trông như thế này:

nhập khẩu funtools

lớp Phương thức lớp:
    "Giả lập PyClassMethod_Type() trong Objects/funcobject.c"

    định nghĩa __init__(tự, f):
        tự.f = f
        functools.update_wrapper(self, f)

    def __get__(self, obj, cls=None):
        nếu cls  Không:
            cls = loại(obj)
        trả về MethodType(self.f, cls)

Lệnh gọi functools.update_wrapper() trong ClassMethod thêm thuộc tính __wrapped__ đề cập đến hàm cơ bản. Ngoài ra, nó còn mang các thuộc tính cần thiết để làm cho trình bao bọc trông giống như hàm được bao bọc: __name__, __qualname__, __doc____annotations__.

Đối tượng thành viên và __slots__

Khi một lớp định nghĩa __slots__, nó sẽ thay thế các từ điển phiên bản bằng một mảng các giá trị vị trí có độ dài cố định. Từ quan điểm người dùng có một số tác dụng:

1. Provides immediate detection of bugs due to misspelled attribute assignments. Only attribute names specified in __slots__ are allowed:

hạng xe:
    __slots__ = ('id_number', 'make', 'model')
>>> auto = Xe()
>>> auto.id_nubmer = 'VYE483814LQEX'
Traceback (cuộc gọi gần đây nhất):
    ...
AttributionError: Đối tượng 'Xe' không có thuộc tính 'id_nubmer'

2. Helps create immutable objects where descriptors manage access to private attributes stored in __slots__:

lớp Bất biến:

    __slots__ = ('_dept', '_name') # Replace từ điển mẫu

    def __init__(bản thân, bộ phận, tên):
        self._dept = dept # Store thành thuộc tính riêng tư
        self._name = tên # Store thành thuộc tính riêng tư

    @property  tả # Read-only
    def dept(self):
        tự trả về._dept

    @property
    tên def (tự):  tả # Read-only
        tự trả về._name
>>> mark = Immutable('Thực vật học', 'Mark Watney')
>>> mark.dept
'Thực vật học'
>>> mark.dept = 'Cướp biển không gian'
Traceback (cuộc gọi gần đây nhất):
    ...
AttributionError: thuộc tính 'dept' của đối tượng 'Immutable' không có setter
>>> mark.location = 'Sao Hỏa'
Traceback (cuộc gọi gần đây nhất):
    ...
AttributionError: Đối tượng 'Bất biến' không có thuộc tính 'vị trí'

3. Saves memory. On a 64-bit Linux build, an instance with two attributes takes 48 bytes with __slots__ and 152 bytes without. This flyweight design pattern likely only matters when a large number of instances are going to be created.

4. Improves speed. Reading instance variables is 35% faster with __slots__ (as measured with Python 3.10 on an Apple M1 processor).

5. Blocks tools like functools.cached_property() which require an instance dictionary to function correctly:

từ functools nhập cached_property

lớp CP:
    __slots__ = () # Eliminates câu lệnh ví dụ

    @cached_property # Requires một ví dụ
    def pi(tự):
        trả về 4 * tổng((-1.0)**n / (2.0*n + 1.0)
                       cho n  dạng đảo ngược(range(100_000)))
>>> CP().pi
Traceback (cuộc gọi gần đây nhất):
  ...
TypeError: Không có thuộc tính '__dict__' trên phiên bản 'CP' để lưu thuộc tính 'pi' vào bộ đệm.

Không thể tạo phiên bản Python thuần túy thả vào chính xác của __slots__ vì nó yêu cầu quyền truy cập trực tiếp vào cấu trúc C và kiểm soát việc phân bổ bộ nhớ đối tượng. Tuy nhiên, chúng ta có thể xây dựng một mô phỏng gần như trung thực trong đó cấu trúc C thực tế cho các vị trí được mô phỏng bằng danh sách _slotvalues riêng. Việc đọc và ghi vào cấu trúc riêng tư đó được quản lý bởi các bộ mô tả thành viên:

null = đối tượng()

Thành viên lớp:

    def __init__(self, name, clsname, offset):
        'Giả lập PyMemberDef trong Bao gồm/structmember.h'
        # Also xem descr_new() trong Objects/descroobject.c
        self.name = tên
        self.clsname = clsname
        self.offset =  đắp

    def __get__(self, obj, objtype=None):
        'Giả lập member_get() trong Objects/descroobject.c'
        # Also xem PyMember_GetOne() trong Python/structmember.c
        nếu obj  Không:
            tự trở về
        value = obj._slotvalues[self.offset]
        nếu giá trị  null:
            tăng AttributionError(self.name)
        giá trị trả về

    def __set__(self, obj, value):
        'Giả lập member_set() trong Objects/descroobject.c'
        obj._slotvalues[self.offset] = giá trị

    def __delete__(self, obj):
        'Giả lập member_delete() trong Objects/descroobject.c'
        value = obj._slotvalues[self.offset]
        nếu giá trị  null:
            tăng AttributionError(self.name)
        obj._slotvalues[self.offset] = null

    chắc chắn __repr__(tự):
        'Giả lập member_repr() trong Objects/descroobject.c'
        return f'<Thành viên {self.name!r} của {self.clsname!r}>'

Phương thức type.__new__() đảm nhiệm việc thêm các đối tượng thành viên vào các biến lớp:

Loại lớp (loại):
    'Mô phỏng cách siêu dữ liệu loại thêm các đối tượng thành viên cho các vị trí'

    def __new__(mcls, clsname, căn cứ, ánh xạ, **kwargs):
        'Emulate type_new() in Objects/typeobject.c'
        # type_new() gọi PyTypeReady() gọi add_methods()
        slot_names = maps.get('slot_names', [])
        để  đắp, tên trong bảng liệt  (slot_names):
            ánh xạ [tên] = Thành viên (tên, clsname, offset)
        kiểu trả về.__new__(mcls, clsname, căn cứ, ánh xạ, **kwargs)

Phương thức object.__new__() đảm nhiệm việc tạo các phiên bản có vị trí thay vì từ điển phiên bản. Đây là một mô phỏng thô bằng Python thuần túy:

Đối tượng lớp:
    'Mô phỏng cách object.__new__() phân bổ bộ nhớ cho __slots__'

    def __new__(cls, *args, **kwargs):
        'Giả lập object_new() trong Objects/typeobject.c'
        inst = super().__new__(cls)
        nếu hasattr(cls, 'slot_names'):
            trống_slots = [null] * len(cls.slot_names)
            object.__setattr__(inst, '_slotvalues',empty_slots)
        trở lại ngay lập tức

    def __setattr__(bản thân, tên, giá trị):
        'Giả lập _PyObject_GenericSetAttrWithDict() Đối tượng/object.c'
        cls = loại (tự)
        nếu hasattr(cls, 'slot_names')  tên không  trong cls.slot_names:
            tăng AttributionError(
                Đối tượng f'{cls.__name__!r} không có thuộc tính {name!r}'
            )
        super().__setattr__(tên, giá trị)

    def __delattr__(bản thân, tên):
        'Giả lập _PyObject_GenericSetAttrWithDict() Đối tượng/object.c'
        cls = loại (tự)
        nếu hasattr(cls, 'slot_names')  tên không  trong cls.slot_names:
            tăng AttributionError(
                Đối tượng f'{cls.__name__!r} không có thuộc tính {name!r}'
            )
        super().__delattr__(tên)

Để sử dụng mô phỏng trong một lớp thực, chỉ cần kế thừa từ Object và đặt metaclass thành Type:

lớp H(Đối tượng, siêu dữ liệu=Loại):
    'Biến thể hiện được lưu trữ trong các vị trí'

    slot_names = ['x', 'y']

    def __init__(self, x, y):
        tự.x = x
        tự.y = y

Tại thời điểm này, siêu dữ liệu đã tải các đối tượng thành viên cho xy:

>>> từ trang nhập pprint
>>> pp(dict(vars(H)))
{'__module__': '__main__',
 '__doc__': 'Biến thực thể được lưu trữ trong các vị trí',
 'slot_names': ['x', 'y'],
 '__init__': <hàm H.__init__ tại 0x7fb5d302f9d0>,
 'x': <Thành viên 'x' của 'H'>,
 'y': <Thành viên 'y' của 'H'>}

Khi các phiên bản được tạo, chúng có danh sách slot_values nơi lưu trữ các thuộc tính:

>>> h = H(10, 20)
>>> vars(h)
{'_slotvalues': [10, 20]}
>>> h.x = 55
>>> vars(h)
{'_slotvalues': [55, 20]}

Các thuộc tính sai chính tả hoặc không được gán sẽ đưa ra một ngoại lệ:

>>> h.xz
Traceback (cuộc gọi gần đây nhất):
    ...
AttributionError: Đối tượng 'H' không có thuộc tính 'xz'