32. Cuộc gọi phương thức
32. Cuộc gọi phương thức
Cuộc gọi phương thức là cuộc gọi hàm thường bắt đầu bằng quyền truy cập thuộc tính. Chúng là trung tâm của mô hình đối tượng của Python vì hầu hết hành vi của đối tượng được thể hiện thông qua các phương thức.
Một cuộc gọi phương thức như:python id="l3owku" obj.method(arg) trông giống như một thao tác, nhưng CPython thực hiện một số bước khái niệm:```text id="j39rdj"
load obj
look up attribute method
bind obj if needed
load arg
call the resolved callable
return result or raise exception
## 32.1 Cuộc gọi phương thức là cuộc gọi truy cập thuộc tính Plus
Biểu thức nguồn:```python id="xgyvgk"
obj.method(10)
```có thể được hiểu là:```python id="gg912h"
tmp = obj.method
tmp(10)
```Đây là mô hình ngữ nghĩa chính xác. Việc tra cứu thuộc tính xảy ra đầu tiên. Kết quả của việc tra cứu đó sau đó được gọi.
Kết quả đó có thể là:```text id="mfp3rd"
a bound method
a function
a built-in method
a callable object
a descriptor result
a property result
any object returned by __getattribute__
```Cuộc gọi chỉ thành công nếu giá trị đã giải quyết có thể gọi được.
Ví dụ:```python id="hsu4g1"
class C:
value = 10
obj = C()
obj.value()
```Điều này thất bại vì`obj.value`quyết tâm`10`và một số nguyên không thể gọi được.
## 32.2 Hàm đơn giản được lưu trữ trên một lớp
Một hàm được định nghĩa bên trong một lớp sẽ trở thành một bộ mô tả.```python id="ys9rpg"
class User:
def greet(self, name):
return "hello " + name
```Từ điển lớp chứa một đối tượng hàm:```python id="vcrq8f"
print(User.__dict__["greet"])
```Khi được truy cập thông qua một thể hiện:```python id="moa6m1"
u = User()
u.greet
```hành vi mô tả của hàm liên kết thể hiện như`self`.
Về mặt khái niệm:```text id="ho345m"
User.__dict__["greet"].__get__(u, User)
-> bound method
```Phương thức ràng buộc lưu trữ:```text id="3tilp4"
function object
self object
```Gọi:```python id="tnkuwp"
u.greet("Ada")
```về mặt khái niệm tương đương với:```python id="zdsl7s"
User.greet(u, "Ada")
```tự động`self`đối số là điểm khác biệt chính giữa lệnh gọi hàm và lệnh gọi phương thức cá thể thông thường.
## 32.3 Phương thức ràng buộc
Một phương thức bị ràng buộc đóng gói một hàm với một đối tượng.```python id="xz4yfy"
class C:
def f(self, x):
return x + 1
obj = C()
m = obj.f
print(m(10))
```Đây,`m`là một phương pháp ràng buộc. Nó nhớ`obj`.
Về mặt khái niệm:```text id="vqipye"
bound method
__func__ -> C.f
__self__ -> obj
```Bạn có thể kiểm tra điều này:```python id="e0hgzo"
print(m.__func__)
print(m.__self__)
```Khi`m(10)`được gọi, CPython gọi hàm cơ bản bằng`obj`được chèn làm đối số đầu tiên:```python id="9rt8ja"
m.__func__(m.__self__, 10)
```## 32.4 Truy cập chức năng không liên kết thông qua lớp
Việc truy cập một phương thức thông qua lớp sẽ cung cấp một đối tượng giống hàm không liên kết một thể hiện theo cách tương tự.```python id="kyy416"
class C:
def f(self, x):
return x + 1
obj = C()
print(C.f(obj, 10))
```Đây,`obj`được thông qua một cách rõ ràng.
Quyền truy cập lớp:```python id="0trmyu"
C.f
```không ràng buộc một trường hợp cụ thể như`self`.
Về mặt khái niệm:```text id="xgfyue"
C.__dict__["f"].__get__(None, C)
-> function object or descriptor result suitable for class access
```Vì vậy, đây là tương đương với các phương thức cá thể thông thường:```python id="1yhu3p"
obj.f(10)
C.f(obj, 10)
```Biểu mẫu đầu tiên thực hiện ràng buộc cá thể. Biểu mẫu thứ hai chuyển thể hiện một cách rõ ràng.
## 32.5 Giao thức mô tả
Liên kết phương thức là một phần của giao thức mô tả.
Bộ mô tả là một đối tượng có một hoặc nhiều phương thức sau:```text id="02rkfq"
__get__
__set__
__delete__
```Các hàm không phải là bộ mô tả dữ liệu vì chúng xác định`__get__`.
Khi CPython đánh giá:```python id="8f0fmh"
obj.method
```và tìm thấy một đối tượng hàm trong lớp, nó gọi logic mô tả của hàm.
Về mặt khái niệm:```python id="q89qnh"
method = function.__get__(obj, type(obj))
```Điều này tạo ra phương thức ràng buộc.
Giao thức mô tả cũng hỗ trợ:```text id="7keg5l"
methods
staticmethod
classmethod
property
slots
many built-in attributes
ORM fields
cached attributes
validation descriptors
```Các cuộc gọi phương thức không thể được hiểu đầy đủ nếu không có bộ mô tả.
## 32.6 Dữ liệu và mô tả phi dữ liệu
Bộ mô tả có hai loại lớn.
| Loại mô tả | Định nghĩa | Ưu tiên |
|---|---|---|
| Bộ mô tả dữ liệu |`__get__`Và`__set__`hoặc`__delete__`| Cao hơn từ điển dụ |
| Bộ mô tả phi dữ liệu | Thông thường chỉ`__get__`| Từ điển thấp hơn ví dụ |
Các hàm thông thường được lưu trữ trên các lớp là các bộ mô tả phi dữ liệu.
Điều này có nghĩa là một thuộc tính thể hiện có thể che khuất một phương thức:```python id="8vtwg1"
class C:
def f(self):
return "method"
obj = C()
obj.f = lambda: "instance function"
print(obj.f())
```Từ điển mẫu chứa`f`, vì vậy nó chiến thắng bộ mô tả phi dữ liệu trong lớp.
Thuộc tính là một bộ mô tả dữ liệu, vì vậy nó chiếm ưu thế trong từ điển mẫu:```python id="tfs80s"
class C:
@property
def x(self):
return 10
```Các quy tắc tra cứu ảnh hưởng đến đối tượng cuối cùng được gọi.
## 32.7 Thứ tự tra cứu thuộc tính cho các phương thức
Để truy cập thuộc tính phiên bản điển hình:```python id="ebj6os"
obj.name
```CPython tuân theo quy trình tra cứu gần như thế này:```text id="r7zrsx"
1. Determine type(obj)
2. Look for name in type and base classes
3. If a data descriptor is found, call its __get__
4. Otherwise, look in obj.__dict__ if present
5. If found in instance dictionary, return it
6. Otherwise, if a non-data descriptor was found, call its __get__
7. Otherwise, return the class attribute
8. Otherwise, call __getattr__ if defined
9. Otherwise, raise AttributeError
```Cuộc gọi phương thức phụ thuộc vào việc tra cứu này. Phương pháp này không chỉ được chọn theo tên. Nó được chọn bởi giao thức tra cứu thuộc tính đầy đủ.
## 32.8 Mã byte cuộc gọi phương thức
Dành cho:```python id="fwr5qr"
def call(obj, x):
return obj.method(x)
```mã byte khái niệm trông giống như:```text id="yqk6u7"
LOAD_FAST obj
LOAD_METHOD method
LOAD_FAST x
CALL 1
RETURN_VALUE
```Các hướng dẫn chính xác khác nhau tùy theo phiên bản Python, nhưng CPython hiện đại phân biệt tải phương thức với tải thuộc tính chung trong các trường hợp phổ biến.
Mục tiêu là tối ưu hóa mẫu này:```python id="2kv28s"
obj.method(arg)
```mà không làm thay đổi kết quả ngữ nghĩa.
## 32.9 Tại sao`LOAD_METHOD`tồn tại
Một cuộc gọi phương thức ngây thơ sẽ thực hiện điều này:```text id="dg1ri1"
LOAD_FAST obj
LOAD_ATTR method
LOAD_FAST arg
CALL 1
```Nếu như`method`là một chức năng bình thường trên lớp,`LOAD_ATTR`tạo ra một đối tượng phương thức bị ràng buộc.
Sau đó`CALL`ngay lập tức gọi nó.
Việc phân bổ phương pháp ràng buộc tạm thời đó thường không cần thiết.
Thay vào đó, một đường dẫn được tối ưu hóa có thể tải:```text id="eeqqg9"
underlying function
self object
```và gọi hàm trực tiếp với`self`chèn vào.
Về mặt khái niệm:```text id="q3brdt"
obj.method(arg)
optimized:
function = C.__dict__["method"]
self = obj
call function(self, arg)
```Điều này tránh việc tạo đối tượng phương thức bị ràng buộc trong trường hợp gọi ngay lập tức thông thường.
## 32.10 Tránh phân bổ phương thức ràng buộc
Hãy xem xét một vòng lặp:```python id="v4zxb6"
for item in items:
obj.process(item)
```Việc triển khai đơn giản có thể phân bổ một phương thức ràng buộc cho mỗi lần lặp:```text id="th33f3"
obj.process -> new bound method
call bound method
discard bound method
```Điều đó sẽ tạo ra lưu lượng truy cập phân bổ và đếm tham chiếu không cần thiết.
Tối ưu hóa cuộc gọi phương thức của CPython tránh điều này trong các trường hợp phổ biến.
Mô hình ngữ nghĩa vẫn còn:```python id="vxkr9d"
tmp = obj.process
tmp(item)
```Nhưng việc thực hiện có thể bỏ qua việc hiện thực hóa`tmp`như một đối tượng heap khi phương thức được gọi ngay lập tức.
## 32.11 Khi các phương thức ràng buộc vẫn được tạo
Một đối tượng phương thức bị ràng buộc vẫn được tạo khi việc truy cập phương thức chính là kết quả.
Ví dụ:```python id="vnqqc0"
m = obj.method
```Ở đây, mã Python yêu cầu giá trị thuộc tính. CPython phải tạo đối tượng phương thức bị ràng buộc vì chương trình có thể lưu trữ, kiểm tra, chuyển nó hoặc gọi nó sau.```python id="ipgvev"
callbacks.append(obj.method)
```Trong trường hợp này, đối tượng phương thức bị ràng buộc là kết quả chính xác mà Python có thể nhìn thấy.
Tối ưu hóa chủ yếu áp dụng cho các mẫu cuộc gọi ngay lập tức trong đó phương thức bị ràng buộc không cần phải thoát.
## 32.12 Các phương thức tích hợp
Các kiểu tích hợp hiển thị nhiều phương thức được triển khai trong C.```python id="dkd1sx"
xs = []
xs.append(1)
```Phương pháp liệt kê`append`được thực hiện trong C.
Một cuộc gọi phương thức tích hợp bao gồm:```text id="8n6a77"
method lookup
binding to list object
argument setup
C method call
mutation of list object
return None
```Đối tượng bị ràng buộc vẫn hiện diện về mặt khái niệm, nhưng việc triển khai có thể được tối ưu hóa cao.
Để nối thêm danh sách, thao tác sẽ thay đổi cấu trúc danh sách cơ bản trực tiếp trong C.
## 32.13 Bộ mô tả phương thức
Các phương thức tích hợp thường được biểu diễn bằng các đối tượng mô tả thay vì các đối tượng hàm Python thông thường.
Ví dụ:```python id="sfrzai"
print(list.__dict__["append"])
```Đây không phải là một hàm Python bình thường. Nó là một bộ mô tả phương thức tích hợp.
Khi được truy cập thông qua một thể hiện:```python id="20nfof"
[].append
```nó tạo ra một đối tượng phương thức tích hợp được liên kết với danh sách đó.
Khi được gọi ngay lập tức:```python id="n6aps6"
[].append(1)
```CPython có thể đi theo con đường riêng chuyên biệt.
## 32,14`staticmethod`
`staticmethod`vô hiệu hóa ràng buộc cá thể.```python id="i095gy"
class Math:
@staticmethod
def add(a, b):
return a + b
Math.add(2, 3)
Math().add(2, 3)
```Trong cả hai trường hợp, không`self`được chèn vào.
Bộ mô tả trả về hàm cơ bản mà không ràng buộc một thể hiện.
Về mặt khái niệm:```text id="1s9dk7"
staticmethod.__get__(obj, cls)
-> original function
```Vì vậy, điều này hoạt động:```python id="opmkar"
Math().add(2, 3)
```vì hàm nhận chính xác hai đối số chứ không phải ba đối số.
## 32,15`classmethod`
`classmethod`liên kết lớp chứ không phải là thể hiện.```python id="dlj84i"
class C:
@classmethod
def make(cls, value):
return cls(value)
```Gọi:```python id="g1ck47"
C.make(10)
```vượt qua`C`như đối số đầu tiên.
Gọi thông qua một thể hiện:```python id="frurij"
obj = C()
obj.make(10)
```cũng vượt qua lớp học, thường là`C`, không`obj`.
Về mặt khái niệm:```text id="2cfzmr"
classmethod.__get__(obj, cls)
-> bound method with cls as first argument
```Đây là lý do tại sao các phương thức lớp lại hữu ích cho các hàm tạo thay thế và cấu trúc đa hình.
## 32,16`property`và quyền truy cập giống như phương thức
Một thuộc tính biến logic phương thức thành quyền truy cập thuộc tính.```python id="b2eu4v"
class C:
@property
def value(self):
return 42
obj = C()
print(obj.value)
```Điều này không gọi`obj.value()`.
Cuộc gọi xảy ra trong quá trình truy cập thuộc tính:```text id="m2zdkj"
obj.value
property.__get__(obj, C)
calls getter function
returns result
```Nếu thuộc tính trả về một lệnh gọi được thì lệnh gọi sau có thể xảy ra:```python id="ylxm9w"
obj.factory()
```Nếu như`factory`là một tài sản, điều này có nghĩa là:```text id="95d6z5"
call property getter
call returned object
```Vì vậy, một lệnh gọi cấp nguồn có thể liên quan đến các lệnh gọi mô tả ẩn trước lệnh gọi rõ ràng.
## 32,17`__getattribute__`Mọi tra cứu thuộc tính bình thường đều trải qua`__getattribute__`.
```python id="e8vyww"
class C:
def __getattribute__(self, name):
print("lookup", name)
return super().__getattribute__(name)
def f(self):
return 1
obj = C()
obj.f()
```biểu thức`obj.f()`cuộc gọi đầu tiên`obj.__getattribute__("f")`.
Điều này có nghĩa là các cuộc gọi phương thức có thể bị chặn.
một phong tục`__getattribute__`Có thể:```text id="xczvn9"
return a normal bound method
return a different callable
return a non-callable
raise AttributeError
log access
implement proxies
implement lazy loading
```Máy gọi không biết mục đích nguồn ban đầu. Nó gọi bất kỳ thuộc tính nào mà việc tra cứu trả về.
## 32,18`__getattr__`
`__getattr__`chỉ được gọi sau khi tra cứu thông thường không thành công.```python id="zewpol"
class Dynamic:
def __getattr__(self, name):
if name == "run":
return lambda: "dynamic"
raise AttributeError(name)
obj = Dynamic()
print(obj.run())
```Đây,`run`không tồn tại trong thể hiện hoặc lớp.`__getattr__`trả về một cuộc gọi được. Cuộc gọi sau đó sẽ gọi ra kết quả có thể gọi được.
Điều này phổ biến ở:```text id="xud82d"
proxies
RPC clients
ORM models
mock objects
lazy APIs
dynamic wrappers
```Điều đó cũng có nghĩa là các cuộc gọi phương thức có thể được giải quyết linh hoạt khi chạy.
## 32.19 Lệnh gọi phương thức trên mô-đun
Các mô-đun cũng có thể hỗ trợ truy cập thuộc tính động với cấp độ mô-đun`__getattr__`.
```python id="m9w5tu"
# module.py
def __getattr__(name):
if name == "run":
return lambda: 42
raise AttributeError(name)
```Sau đó:```python id="pkzy1x"
import module
module.run()
```có thể giải quyết một cách linh hoạt.
Đường dẫn tra cứu khác với liên kết phương thức phiên bản, nhưng mẫu nguồn vẫn là quyền truy cập thuộc tính theo sau là lệnh gọi.
## 32.20 Thứ tự giải quyết phương pháp
Đối với các thể hiện của lớp, tra cứu phương thức sẽ tìm kiếm lớp và các lớp cơ sở của nó bằng cách sử dụng thứ tự phân giải phương thức.```python id="pvvfui"
class A:
def f(self):
return "A"
class B(A):
pass
obj = B()
print(obj.f())
```Các tìm kiếm tra cứu`B`, sau đó`A`.
Đối với đa kế thừa:```python id="f6sw15"
class A:
def f(self):
return "A"
class B:
def f(self):
return "B"
class C(A, B):
pass
```Phương pháp được lựa chọn phụ thuộc vào`C.__mro__`.
```python id="ig5h2o"
print(C.__mro__)
```MRO là trung tâm của các cuộc gọi phương thức vì nó xác định nơi tra cứu thuộc tính lớp tìm thấy bộ mô tả.
## 32,21`super()`Cuộc gọi phương thức`super()`những thay đổi nơi việc tra cứu phương thức bắt đầu trong MRO.```python id="ujmrwo"
class Base:
def f(self):
return 1
class Child(Base):
def f(self):
return super().f() + 1
```Cuộc gọi:```python id="w4m10h"
super().f()
```không có nghĩa là “gọi tên lớp cha”. Nó có nghĩa là tìm kiếm MRO sau lớp hiện tại, có liên kết với phiên bản hiện tại.
Về mặt khái niệm:```text id="nlfcwi"
current class = Child
instance = self
MRO = [Child, Base, object]
search after Child
find Base.f
bind to self
call
```Kết quả là một phương thức bị ràng buộc sử dụng cùng một thể hiện.
## 32.22 Phương thức và Kế thừa
Một cuộc gọi phương thức có thể sử dụng một phương thức được kế thừa từ một lớp cơ sở.```python id="xtljqg"
class Base:
def save(self):
return "saved"
class User(Base):
pass
u = User()
u.save()
```Việc tra cứu tìm thấy`save`TRONG`Base`, sau đó liên kết nó với`u`.
Về mặt khái niệm:```text id="e2s1rr"
find Base.__dict__["save"]
bind with self = u
call Base.save(u)
```Lớp xác định của hàm và lớp thực tế của thể hiện có thể khác nhau. Điều này là bình thường.
## 32.23 Phương thức ghi đè
Các phương thức lớp con ghi đè các phương thức cơ sở.```python id="3eakv8"
class Base:
def f(self):
return "base"
class Child(Base):
def f(self):
return "child"
print(Child().f())
```Tra cứu tìm thấy`Child.f`trước`Base.f`.
Đây là tra cứu thời gian chạy. Cuộc gọi không bị ràng buộc tĩnh bởi loại biến.```python id="43x5xb"
def call_f(obj):
return obj.f()
```Phương pháp được lựa chọn phụ thuộc vào`type(obj)`vào thời gian chạy.
## 32.24 Lệnh gọi phương thức đa hình
Các cuộc gọi phương thức Python được gửi động.```python id="uwb85l"
def speak(animal):
return animal.speak()
```Các đối tượng khác nhau có thể cung cấp các cách triển khai khác nhau:```python id="fcv0ik"
class Dog:
def speak(self):
return "woof"
class Cat:
def speak(self):
return "meow"
```Cùng một mã byte có thể gọi các phương thức khác nhau tùy thuộc vào đối tượng thời gian chạy.
Về mặt khái niệm:```text id="cje20f"
LOAD_FAST animal
LOAD_METHOD speak
CALL 0
```Mục tiêu thực tế được phát hiện trong quá trình thực hiện.
Công văn động này linh hoạt nhưng tạo ra những thách thức tối ưu hóa.
## 32.25 Trang web gọi đơn hình và đa hình
Một trang web gọi phương thức là đơn hình nếu nó thường nhìn thấy một loại đối tượng.```python id="j7bxa8"
for user in users:
user.validate()
```Nếu mọi`user`có cùng loại, trang gọi là đơn hình.
Một trang web cuộc gọi là đa hình nếu nó thấy một số loại:```python id="kw1tz2"
for shape in shapes:
shape.area()
```Ở đâu`shape`Có lẽ`Circle`, `Square`, hoặc`Triangle`.
Bộ nhớ đệm nội tuyến hoạt động tốt nhất khi các trang web cuộc gọi ổn định. Một trang gọi đơn hình có thể lưu vào bộ nhớ đệm thông tin tra cứu loại và phương thức hiệu quả hơn.
## 32.26 Bộ nhớ đệm nội tuyến cho lệnh gọi phương thức
CPython có thể lưu trữ thông tin tra cứu phương thức gần lệnh mã byte.
Bộ đệm phương thức có thể ghi lại các sự kiện như:```text id="cdvqx9"
expected receiver type
type version tag
resolved descriptor
method object or function pointer
offset or lookup result
call shape
```Trong lần thực hiện tiếp theo:```text id="9o02ps"
if receiver type still matches
and type version is unchanged
use cached method path
else
fall back to generic lookup
```Điều này tăng tốc các cuộc gọi lặp lại mà không thay đổi ngữ nghĩa.
Nếu một lớp được sửa đổi, thẻ phiên bản hoặc bộ bảo vệ bộ đệm sẽ vô hiệu hóa đường dẫn nhanh.
## 32.27 Đột biến lớp và vô hiệu hóa bộ đệm
Python cho phép các lớp thay đổi khi chạy.```python id="jtw9f0"
class C:
def f(self):
return 1
obj = C()
print(obj.f())
def new_f(self):
return 2
C.f = new_f
print(obj.f())
```Cuộc gọi thứ hai phải sử dụng phương thức mới.
Do đó, bất kỳ bộ đệm nào sử dụng phương thức cũ đều phải bị vô hiệu hóa hoặc được bảo vệ.
Quy tắc an toàn:```text id="89t5m7"
fast method path is valid only while class and lookup assumptions remain true
```Đột biến lớp động là một lý do khiến tối ưu hóa CPython sử dụng các bộ bảo vệ.
## 32.28 Đột biến từ điển sơ thẩm
Thuộc tính phiên bản cũng có thể ảnh hưởng đến việc tra cứu phương thức cho các bộ mô tả phi dữ liệu.```python id="4tnwq5"
class C:
def f(self):
return "class method"
obj = C()
obj.f = lambda: "instance value"
print(obj.f())
```Mục nhập từ điển phiên bản che mờ hàm lớp vì các hàm thông thường không phải là bộ mô tả dữ liệu.
Bộ đệm phương thức phải tính đến trạng thái từ điển khi có liên quan.
Đây là một lý do khác khiến việc tra cứu phương thức phức tạp hơn việc nhảy trực tiếp vào bảng lớp.
## 32,29 Slots và lệnh gọi phương thức
Lớp học với`__slots__`có thể không có từ điển mẫu thông thường.```python id="vhbgrn"
class C:
__slots__ = ("x",)
def f(self):
return self.x
```Các vị trí ảnh hưởng đến việc lưu trữ thuộc tính, nhưng việc tra cứu phương thức vẫn tìm kiếm lớp và cơ sở của nó.
Sự vắng mặt của`__dict__`có thể đơn giản hóa một số trường hợp thuộc tính, nhưng các bộ mô tả, sự kế thừa và đột biến lớp động vẫn còn quan trọng.
## 32.30 Phương pháp đặc biệt
Các phương pháp đặc biệt như`__len__`, `__add__`, Và`__iter__`thường được tra cứu thông qua các vị trí loại thay vì tra cứu thuộc tính phiên bản thông thường.
Ví dụ:```python id="k6wyb3"
len(obj)
```không chỉ đơn giản là thực thi:```python id="9dm1lp"
obj.__len__()
```về mọi mặt. CPython thường sử dụng khe của loại cho độ dài.
Sự khác biệt này quan trọng:```python id="0t598r"
class C:
def __len__(self):
return 10
obj = C()
obj.__len__ = lambda: 20
print(len(obj))
print(obj.__len__())
```Cuộc gọi phương thức rõ ràng có thể sử dụng thuộc tính instance. các`len()`hoạt động sử dụng phương pháp tra cứu đặc biệt thông qua kiểu.
Các phương pháp đặc biệt được tối ưu hóa và tích hợp vào các khe giao thức đối tượng.
## 32.31 Cuộc gọi của nhà điều hành so với Cuộc gọi phương thức
Các toán tử thường ánh xạ tới các phương thức đặc biệt.```python id="3pof5j"
a + b
```có thể gọi:```text id="pzts11"
a.__add__(b)
b.__radd__(a)
```Nhưng CPython không hoạt động bình thường`obj.__add__`tra cứu thuộc tính cho mỗi lần bổ sung. Nó sử dụng các khe số trên các đối tượng loại.
Vì thế:```python id="rizu1b"
obj.method()
```Và:```python id="g6n28n"
obj + other
```đều là công văn động, nhưng chúng sử dụng các đường dẫn nội bộ khác nhau.
Cuộc gọi phương thức sử dụng tra cứu thuộc tính và máy gọi. Người vận hành sử dụng các khe giao thức, với hành vi dự phòng.
## 32.32 Lệnh gọi phương thức và số lượng tham chiếu
Một cuộc gọi phương thức phải được duy trì:```text id="nb20se"
receiver object
resolved callable
arguments
temporary bound method, if created
return value
exception state, if raised
```Đối với lệnh gọi phương thức được tối ưu hóa nhằm tránh đối tượng phương thức bị ràng buộc, CPython vẫn cần đảm bảo bộ thu vẫn hoạt động trong khi hàm cơ bản chạy.
Về mặt khái niệm:```text id="7pxphh"
load receiver
resolve method
prepare self and args
call
release temporaries
push result
```Việc xử lý tham chiếu không chính xác ở đây có thể gây ra lỗi nghiêm trọng vì các lệnh gọi phương thức thường nhập lại Python và mã tùy ý có thể chạy.
## 32.33 Cuộc gọi phương thức có thể nhập lại Python
Bản thân phương thức tra cứu có thể thực thi mã Python.
Ví dụ:```text id="av2mft"
custom __getattribute__
descriptor __get__
property getter
__getattr__
metaclass attribute lookup
```Sau đó, lệnh gọi phương thức đã giải quyết có thể thực thi nhiều mã Python hơn.
Một biểu thức nguồn duy nhất:```python id="1eq7mu"
obj.method(arg)
```có thể liên quan đến:```text id="44juqs"
call __getattribute__
call descriptor __get__
call method body
```Mỗi cuộc gọi có thể nâng cao, thay đổi trạng thái hoặc thay đổi hành vi tra cứu trong tương lai.
## 32.34 Lệnh gọi phương thức và ngoại lệ
Một cuộc gọi phương thức có thể thất bại ở nhiều điểm:```text id="8b856f"
receiver expression raises
attribute lookup raises AttributeError or another exception
descriptor binding raises
argument expression raises
resolved object is not callable
argument binding fails
method body raises
return cleanup raises indirectly
```Ví dụ:```python id="q0kkqy"
obj.missing()
```thất bại trong quá trình tra cứu thuộc tính.
Ví dụ:```python id="68k1hw"
obj.method(bad())
```có thể thất bại trong khi đánh giá đối số trước khi phương thức được gọi.
Ví dụ:```python id="ex1xt4"
obj.method()
```có thể thất bại bên trong thân phương thức.
Đường dẫn lỗi mã byte phải xóa các giá trị ngăn xếp tạm thời trong mọi trường hợp.
## 32.35 Cuộc gọi phương thức và`None`Một lỗi phổ biến:```python id="ov87fl"
xs = []
result = xs.append(1)
result.append(2)
list.appendtrả lạiNone.
Dòng thứ hai thất bại vìresultlàNone, không phải danh sách.
Ở cấp độ gọi phương thức:text id="dx8gic" xs.append(1) mutates xs returns None Giá trị trả về vẫn được đẩy bởi lệnh gọi, sau đó được lưu trữ trongresult.
Các cuộc gọi phương thức không ngụ ý chuỗi trôi chảy trừ khi phương thức đó trả về một cách rõ ràngselfhoặc một vật thể khác.
32.36 Cuộc gọi phương thức theo chuỗi
Cuộc gọi theo chuỗi thực hiện từ trái sang phải.python id="jxorjs" obj.a().b().c() Về mặt khái niệm:```text id="dylm9m"
tmp1 = obj.a()
tmp2 = tmp1.b()
tmp3 = tmp2.c()
Nếu như`a()`trả lại`None`, sau đó`.b()`thất bại.
Mã byte sử dụng ngăn xếp để giữ từng kết quả trung gian đủ lâu để truy cập phương thức tiếp theo.
## 32.37 API thông thạo
Một số API cố tình trả lại`self`:
```python id="36isx5"
class Builder:
def set_name(self, name):
self.name = name
return self
def set_age(self, age):
self.age = age
return self
builder = Builder().set_name("Ada").set_age(37)
```Mỗi phương thức làm thay đổi đối tượng và trả về nó.
Bộ máy gọi phương thức là bình thường. Phong cách trôi chảy là quy ước của thư viện, không phải là tính năng phiên dịch đặc biệt.
## 32.38 Các phương thức như đối tượng hạng nhất
Các phương thức có thể được lưu trữ và truyền đi.```python id="63bqx2"
class C:
def f(self, x):
return x + 1
obj = C()
callback = obj.f
print(callback(10))
```Phương pháp ràng buộc giữ`obj`còn sống.
Về mặt khái niệm:```text id="jrk395"
callback
function = C.f
self = obj
```Điều này có thể ảnh hưởng đến tuổi thọ bộ nhớ:```python id="ka89yy"
callbacks.append(obj.method)
```Danh sách gọi lại hiện giữ cho đối tượng tồn tại thông qua phương thức bị ràng buộc.
## 32.39 Phương thức gọi và thu gom rác
Các phương thức ràng buộc có thể tham gia vào các biểu đồ tham chiếu.
Ví dụ:```python id="7qj87e"
class C:
def f(self):
return 1
obj = C()
obj.callback = obj.f
```Hiện nay:```text id="7udx0g"
obj
-> callback bound method
-> self obj
```Điều này tạo ra một chu kỳ.
Trình thu gom rác theo chu kỳ của CPython có thể thu thập các chu kỳ như vậy nếu chúng không thể truy cập được và nếu các quy tắc quyết toán cho phép dọn dẹp.
Đây là một ví dụ thực tế về cách các đối tượng phương thức kết nối với quản lý bộ nhớ.
## 32.40 Phương pháp kiểm tra
Bạn có thể kiểm tra các đối tượng phương thức:```python id="dcn13l"
class C:
def f(self, x):
return x + 1
obj = C()
m = obj.f
print(type(m))
print(m.__func__)
print(m.__self__)
```Bạn có thể kiểm tra nội dung từ điển lớp:```python id="brp4q2"
print(C.__dict__["f"])
```Bạn có thể kiểm tra mã byte:```python id="xq1plc"
import dis
def call(obj, x):
return obj.f(x)
dis.dis(call)
```Điều này cho phép bạn xem liệu trình biên dịch có đưa ra các hướng dẫn dành riêng cho phương pháp cho phiên bản Python của bạn hay không.
## 32.41 Mô hình liên kết phương thức tối thiểu
Mô hình mô tả đồ chơi:```python id="b5ukc4"
class Function:
def __init__(self, code):
self.code = code
def __get__(self, obj, cls):
if obj is None:
return self
return BoundMethod(self, obj)
def __call__(self, *args):
return self.code(*args)
class BoundMethod:
def __init__(self, func, self_obj):
self.__func__ = func
self.__self__ = self_obj
def __call__(self, *args):
return self.__func__(self.__self__, *args)
```Sử dụng nó:```python id="f8zqma"
def body(self, x):
return self.value + x
class C:
value = 10
C.f = Function(body)
obj = C()
print(obj.f(5))
```Đây không phải là cách triển khai của CPython, nhưng nó nắm bắt được ý tưởng ràng buộc:```text id="lybvyc"
function stored on class
access through instance
descriptor creates bound method
call inserts self
```## 32.42 Những hiểu lầm phổ biến
| Hiểu lầm | Đúng mẫu |
|---|---|
|`obj.method(x)`gọi trực tiếp một hàm được lưu trữ trên`obj`| Nó thực hiện tra cứu thuộc tính, liên kết, sau đó gọi |
|`self`là từ khóa được chèn theo cú pháp |`self`chỉ là đối số đầu tiên theo quy ước |
| Các phương thức luôn tồn tại trong các phiên bản | Các phương thức thông thường tồn tại trên các lớp và liên kết với các thể hiện |
| Các phương thức ràng buộc luôn được phân bổ | CPython có thể tránh phân bổ cho các cuộc gọi ngay lập tức |
|`staticmethod`nhận được`self`| Nó không nhận được đối số đầu tiên tự động |
|`classmethod`nhận được phiên bản | Nó nhận được lớp |
| Các phương thức đặc biệt luôn được tra cứu như các phương thức thông thường | Nhiều thứ được giải quyết thông qua các khe loại |
| Tra cứu phương thức tĩnh | Nó phụ thuộc vào loại thời gian chạy, MRO, bộ mô tả và trạng thái phiên bản |
## 32.43 Chiến lược đọc
Để nghiên cứu các lệnh gọi phương thức, hãy bắt đầu với chương trình này:```python id="fybmbx"
class C:
def f(self, x):
return x + 1
def call(obj):
return obj.f(10)
```Thanh tra:```python id="r4y4v3"
import dis
dis.dis(call)
obj = C()
m = obj.f
print(m.__func__)
print(m.__self__)
print(C.__dict__["f"])
```Sau đó thay đổi lớp:```python id="10svlx"
@staticmethod
def s(x): ...
@classmethod
def c(cls, x): ...
@property
def p(self): ...
```Ngoài ra kiểm tra:```python id="p178dp"
obj.f = lambda x: x * 2
```và kiểm tra cách tra cứu thay đổi.
Điều này cho thấy sự tương tác giữa các bộ mô tả, từ điển phiên bản, mã byte và máy gọi.
## 32.44 Tóm tắt chương
Một cuộc gọi phương thức là tra cứu thuộc tính, theo sau là một cuộc gọi. Đối với các phương thức cá thể thông thường, các bộ mô tả hàm liên kết đối tượng nhận làm đối số đầu tiên, tạo ra một phương thức liên kết tương đương về mặt khái niệm với việc gọi hàm lớp với cá thể được chèn vào.
Mô hình cốt lõi là:```text id="xs3s0t"
obj.method(arg)
↓
lookup "method" on obj
↓
apply descriptor binding if needed
↓
obtain callable
↓
call callable with arguments
↓
return result or raise exception
```CPython tối ưu hóa đường dẫn này rất nhiều. Nó có thể tránh việc phân bổ phương thức bị ràng buộc tạm thời, tra cứu phương thức bộ đệm, các trang gọi ổn định chuyên biệt và gọi các phương thức tích hợp thông qua đường dẫn C nhanh.
Ngữ nghĩa vẫn năng động. Loại thời gian chạy, MRO, bộ mô tả, từ điển phiên bản, móc thuộc tính tùy chỉnh, đột biến lớp và các quy tắc phương thức đặc biệt đều ảnh hưởng đến phương thức nào thực sự được gọi.