Lập trình FAQ

Câu hỏi chung

Có trình gỡ lỗi cấp mã nguồn có điểm ngắt và bước đơn không?

Vâng.

Một số trình gỡ lỗi cho Python được mô tả bên dưới và hàm breakpoint() tích hợp cho phép bạn sử dụng bất kỳ trình gỡ lỗi nào trong số đó.

Mô-đun pdb là trình gỡ lỗi chế độ bảng điều khiển đơn giản nhưng đầy đủ cho Python. Nó là một phần của thư viện Python tiêu chuẩn và là documented in the Library Reference Manual. Bạn cũng có thể viết trình gỡ lỗi của riêng mình bằng cách sử dụng mã cho pdb làm ví dụ.

Môi trường phát triển tương tác IDLE, là một phần của bản phân phối Python tiêu chuẩn (thường có sẵn dưới dạng idlelib), bao gồm trình gỡ lỗi đồ họa.

PythonWin là một IDE Python bao gồm trình gỡ lỗi GUI dựa trên pdb. Trình gỡ lỗi PythonWin tô màu các điểm dừng và có khá nhiều tính năng thú vị như gỡ lỗi các chương trình không phải PythonWin. PythonWin có sẵn như một phần của dự án pywin32 và là một phần của bản phân phối ActivePython.

Eric là IDE được xây dựng trên PyQt và thành phần chỉnh sửa Scintilla.

trepan3k là trình gỡ lỗi giống gdb.

Visual Studio Code là IDE có các công cụ gỡ lỗi tích hợp với phần mềm kiểm soát phiên bản.

Có một số IDE Python thương mại bao gồm trình gỡ lỗi đồ họa. Chúng bao gồm:

Có công cụ nào giúp tìm lỗi hoặc thực hiện phân tích tĩnh không?

Vâng.

Ruff, PylintPyflakes thực hiện kiểm tra cơ bản sẽ giúp bạn phát hiện lỗi sớm hơn.

Trình kiểm tra loại tĩnh như mypy, ty, Pyreflypytype có thể kiểm tra gợi ý loại trong mã nguồn Python.

Làm cách nào tôi có thể tạo tệp nhị phân độc lập từ tập lệnh Python?

Bạn không cần khả năng biên dịch mã Python sang C nếu tất cả những gì bạn muốn là một chương trình độc lập mà người dùng có thể tải xuống và chạy mà không cần phải cài đặt bản phân phối Python trước. Có một số công cụ xác định tập hợp các mô-đun mà chương trình yêu cầu và liên kết các mô-đun này với nhau bằng tệp nhị phân Python để tạo ra một tệp thực thi duy nhất.

Một là sử dụng công cụ đóng băng, có trong cây nguồn Python dưới dạng Tools/freeze. Nó chuyển đổi mã byte Python thành mảng C; với trình biên dịch C, bạn có thể nhúng tất cả các mô-đun của mình vào một chương trình mới, sau đó chương trình này được liên kết với các mô-đun Python tiêu chuẩn.

Nó hoạt động bằng cách quét đệ quy nguồn của bạn để tìm các câu lệnh nhập (ở cả hai dạng) và tìm kiếm các mô-đun trong đường dẫn Python tiêu chuẩn cũng như trong thư mục nguồn (đối với các mô-đun tích hợp sẵn). Sau đó, nó biến mã byte cho các mô-đun được viết bằng Python thành mã C (bộ khởi tạo mảng có thể được chuyển thành đối tượng mã bằng cách sử dụng mô-đun soái ca) và tạo tệp cấu hình tùy chỉnh chỉ chứa các mô-đun tích hợp thực sự được sử dụng trong chương trình. Sau đó, nó biên dịch mã C được tạo và liên kết nó với phần còn lại của trình thông dịch Python để tạo thành một tệp nhị phân độc lập hoạt động giống hệt như tập lệnh của bạn.

Các gói sau có thể giúp tạo bảng điều khiển và tệp thực thi GUI:

Có tiêu chuẩn mã hóa hoặc hướng dẫn phong cách nào cho các chương trình Python không?

Vâng. Kiểu mã hóa cần thiết cho các mô-đun thư viện tiêu chuẩn được ghi lại dưới dạng PEP 8.

Ngôn ngữ cốt lõi

Tại sao tôi nhận được UnboundLocalError khi biến có giá trị?

Có thể bạn sẽ ngạc nhiên khi nhận được UnboundLocalError trong mã đang hoạt động trước đó khi nó được sửa đổi bằng cách thêm câu lệnh gán vào đâu đó trong phần nội dung của hàm.

Mã này:

>>> x = 10
>>> def bar():
...     print(x)
...
>>> bar()
10

hoạt động, nhưng mã này:

>>> x = 10
>>> def foo():
...     print(x)
...     x += 1

kết quả là UnboundLocalError:

>>> foo()
Traceback (most recent call last):
  ...
UnboundLocalError: cannot access local variable 'x' where it is not associated with a value

Điều này là do khi bạn thực hiện gán cho một biến trong phạm vi, biến đó sẽ trở thành cục bộ trong phạm vi đó và che khuất bất kỳ biến có tên tương tự nào trong phạm vi bên ngoài. Vì câu lệnh cuối cùng trong foo gán một giá trị mới cho x nên trình biên dịch sẽ nhận ra nó là một biến cục bộ. Do đó, khi print(x) trước đó cố gắng in biến cục bộ chưa được khởi tạo và sẽ xảy ra lỗi.

Trong ví dụ trên, bạn có thể truy cập biến phạm vi bên ngoài bằng cách khai báo nó toàn cục:

>>> x = 10
>>> def foobar():
...     global x
...     print(x)
...     x += 1
...
>>> foobar()
10

Việc khai báo rõ ràng này là bắt buộc để nhắc nhở bạn rằng (không giống như tình huống tương tự bề ngoài với các biến lớp và biến thể) bạn thực sự đang sửa đổi giá trị của biến trong phạm vi bên ngoài:

>>> print(x)
11

Bạn có thể thực hiện điều tương tự trong phạm vi lồng nhau bằng từ khóa nonlocal:

>>> def foo():
...    x = 10
...    def bar():
...        nonlocal x
...        print(x)
...        x += 1
...    bar()
...    print(x)
...
>>> foo()
10
11

Các quy tắc cho các biến cục bộ và toàn cục trong Python là gì?

Trong Python, các biến chỉ được tham chiếu bên trong một hàm hoàn toàn mang tính toàn cục. Nếu một biến được gán một giá trị ở bất kỳ đâu trong phần thân của hàm thì biến đó được coi là cục bộ trừ khi được khai báo rõ ràng là toàn cục.

Mặc dù lúc đầu hơi ngạc nhiên, nhưng chỉ cần cân nhắc kỹ một chút sẽ giải thích được điều này. Một mặt, việc yêu cầu global cho các biến được chỉ định sẽ tạo ra rào cản chống lại các tác dụng phụ ngoài ý muốn. Mặt khác, nếu global được yêu cầu cho tất cả các tham chiếu chung thì bạn sẽ luôn sử dụng global. Bạn sẽ phải khai báo là toàn cục mọi tham chiếu đến hàm tích hợp hoặc thành phần của mô-đun đã nhập. Sự lộn xộn này sẽ làm mất đi tính hữu ích của việc khai báo global trong việc xác định các tác dụng phụ.

Tại sao lambdas được xác định trong một vòng lặp với các giá trị khác nhau đều trả về cùng một kết quả?

Giả sử bạn sử dụng vòng lặp for để xác định một vài lambda khác nhau (hoặc thậm chí các hàm đơn giản), ví dụ:

>>> hình vuông = []
>>> cho x trong phạm vi (5):
... Squares.append(lambda: x**2)

Điều này cung cấp cho bạn một danh sách chứa 5 lambda tính toán x**2. Bạn có thể mong đợi rằng, khi được gọi, chúng sẽ trả về lần lượt là 0, 1, 4, 916. Tuy nhiên, khi bạn thực sự thử bạn sẽ thấy rằng tất cả đều trả về 16:

>>> hình vuông[2]()
16
>>> hình vuông[4]()
16

Điều này xảy ra vì x không phải là cục bộ của lambda mà được xác định ở phạm vi bên ngoài và nó được truy cập khi lambda được gọi --- không phải khi nó được xác định. Ở cuối vòng lặp, giá trị của x4, vì vậy tất cả các hàm bây giờ đều trả về 4**2, tức là 16. Bạn cũng có thể xác minh điều này bằng cách thay đổi giá trị của x và xem kết quả của lambdas thay đổi như thế nào

>>> x = 8
>>> hình vuông[2]()
64

Để tránh điều này, bạn cần lưu các giá trị trong các biến cục bộ vào lambda để chúng không phụ thuộc vào giá trị của x toàn cục:

>>> hình vuông = []
>>> cho x trong phạm vi (5):
... Squares.append(lambda n=x: n**2)

Ở đây, n=x tạo một biến mới n cục bộ cho lambda và được tính khi lambda được xác định sao cho nó có cùng giá trị mà x có tại thời điểm đó trong vòng lặp. Điều này có nghĩa là giá trị của n sẽ là 0 trong lambda đầu tiên, 1 trong lambda thứ hai, 2 trong lambda thứ ba, v.v. Do đó, mỗi lambda bây giờ sẽ trả về kết quả chính xác

>>> hình vuông[2]()
4
>>> hình vuông[4]()
16

Lưu ý rằng hành vi này không phải là đặc thù của lambdas nhưng cũng áp dụng cho các hàm thông thường.

Làm cách nào để chia sẻ các biến toàn cục giữa các mô-đun?

Cách thông thường để chia sẻ thông tin giữa các mô-đun trong một chương trình là tạo một mô-đun đặc biệt (thường được gọi là config hoặc cfg). Chỉ cần nhập mô-đun cấu hình trong tất cả các mô-đun ứng dụng của bạn; mô-đun sau đó sẽ có sẵn dưới dạng tên chung. Vì chỉ có một phiên bản của mỗi mô-đun nên mọi thay đổi được thực hiện đối với đối tượng mô-đun sẽ được phản ánh ở mọi nơi. Ví dụ:

config.py:

x = 0 giá trị # Default của cài đặt cấu hình 'x'

mod.py:

nhập cấu hình
cấu hình.x = 1

main.py:

nhập cấu hình
nhập mod
in(config.x)

Lưu ý rằng việc sử dụng mô-đun cũng là cơ sở để triển khai mẫu thiết kế đơn vì lý do tương tự.

"Các phương pháp hay nhất" để sử dụng tính năng nhập trong mô-đun là gì?

Nói chung là không dùng from modulename import *. Làm như vậy sẽ làm tắc nghẽn không gian tên của nhà nhập khẩu và khiến cho linters khó phát hiện ra những tên không xác định hơn.

Nhập mô-đun ở đầu tệp. Làm như vậy sẽ làm rõ những mô-đun nào khác mà mã của bạn yêu cầu và tránh các câu hỏi về việc liệu tên mô-đun có nằm trong phạm vi hay không. Việc sử dụng một lần nhập trên mỗi dòng giúp dễ dàng thêm và xóa các lần nhập mô-đun, nhưng việc sử dụng nhiều lần nhập trên mỗi dòng sẽ sử dụng ít không gian màn hình hơn.

Đó là một cách thực hành tốt nếu bạn nhập mô-đun theo thứ tự sau:

  1. mô-đun thư viện tiêu chuẩn -- chẳng hạn như sys, os, argparse, re

  2. mô-đun thư viện của bên thứ ba (bất kỳ thứ gì được cài đặt trong thư mục gói trang web của Python) -- chẳng hạn như dateutil, requests, tzdata

  3. mô-đun được phát triển tại địa phương

Đôi khi cần phải di chuyển việc nhập vào một hàm hoặc lớp để tránh các vấn đề với việc nhập tuần hoàn. Gordon McMillan nói:

Nhập vòng tròn sẽ ổn khi cả hai mô-đun đều sử dụng hình thức nhập "nhập <mô-đun>". Chúng không thành công khi mô-đun thứ 2 muốn lấy tên từ tên đầu tiên ("từ tên nhập mô-đun") và quá trình nhập ở cấp cao nhất. Đó là vì tên ở phần đầu tiên vẫn chưa có, vì mô-đun đầu tiên đang bận nhập mô-đun thứ hai.

Trong trường hợp này, nếu mô-đun thứ hai chỉ được sử dụng trong một chức năng thì việc nhập có thể dễ dàng được chuyển sang chức năng đó. Vào thời điểm quá trình nhập được gọi, mô-đun đầu tiên sẽ khởi tạo xong và mô-đun thứ hai có thể thực hiện quá trình nhập.

Cũng có thể cần phải chuyển nội dung nhập ra khỏi cấp mã cao nhất nếu một số mô-đun dành riêng cho nền tảng. Trong trường hợp đó, thậm chí có thể không nhập được tất cả các mô-đun ở đầu tệp. Trong trường hợp này, việc nhập đúng mô-đun vào mã dành riêng cho nền tảng tương ứng là một lựa chọn tốt.

Chỉ di chuyển quá trình nhập vào phạm vi cục bộ, chẳng hạn như bên trong định nghĩa hàm, nếu cần giải quyết vấn đề như tránh nhập vòng tròn hoặc đang cố gắng giảm thời gian khởi tạo của mô-đun. Kỹ thuật này đặc biệt hữu ích nếu nhiều nội dung nhập không cần thiết tùy thuộc vào cách chương trình thực thi. Bạn cũng có thể muốn chuyển nội dung nhập vào một hàm nếu mô-đun chỉ được sử dụng trong hàm đó. Lưu ý rằng việc tải một mô-đun lần đầu tiên có thể tốn kém do việc khởi tạo mô-đun một lần, nhưng việc tải một mô-đun nhiều lần hầu như miễn phí, chỉ tốn một vài lần tra cứu từ điển. Ngay cả khi tên mô-đun nằm ngoài phạm vi, mô-đun này vẫn có thể có sẵn trong sys.modules.

Tại sao các giá trị mặc định được chia sẻ giữa các đối tượng?

Loại lỗi này thường ảnh hưởng đến các lập trình viên mới vào nghề. Hãy xem xét chức năng này:

def foo(mydict={}): # Danger: chia sẻ tham chiếu đến một dict cho tất cả các cuộc gọi
    ... tính toán cái  đó...
    mydict[key] = giá trị
    trả lại mydict

Lần đầu tiên bạn gọi hàm này, mydict chỉ chứa một mục duy nhất. Lần thứ hai, mydict chứa hai mục vì khi foo() bắt đầu thực thi, mydict bắt đầu với một mục đã có trong đó.

Người ta thường mong đợi rằng một lệnh gọi hàm sẽ tạo ra các đối tượng mới cho các giá trị mặc định. Đây không phải là những gì xảy ra. Các giá trị mặc định được tạo chính xác một lần khi hàm được xác định. Nếu đối tượng đó bị thay đổi, giống như từ điển trong ví dụ này, các lệnh gọi hàm tiếp theo sẽ tham chiếu đến đối tượng đã thay đổi này.

Theo định nghĩa, các đối tượng bất biến như số, chuỗi, bộ dữ liệu và None sẽ an toàn trước sự thay đổi. Những thay đổi đối với các đối tượng có thể thay đổi như từ điển, danh sách và thể hiện của lớp có thể dẫn đến nhầm lẫn.

Do tính năng này, cách lập trình tốt là không sử dụng các đối tượng có thể thay đổi làm giá trị mặc định. Thay vào đó, hãy sử dụng None làm giá trị mặc định và bên trong hàm, kiểm tra xem tham số có phải là None hay không và tạo danh sách/từ điển/bất kỳ giá trị nào nếu có. Ví dụ: không viết:

def foo(mydict={}):
    ...

nhưng:

def foo(mydict=Không):
    nếu mydict  Không:
        mydict = {} # create một dict mới cho không gian tên cục bộ

Tính năng này có thể hữu ích. Khi bạn có một hàm tốn nhiều thời gian để tính toán, một kỹ thuật phổ biến là lưu các tham số và giá trị kết quả của mỗi lệnh gọi hàm vào bộ nhớ đệm, đồng thời trả về giá trị đã lưu trong bộ nhớ đệm nếu yêu cầu lại cùng một giá trị. Điều này được gọi là "ghi nhớ" và có thể được triển khai như thế này

# Callers chỉ có thể cung cấp hai tham số và tùy ý chuyển _cache theo từ khóa
def đắt(arg1, arg2, *, _cache={}):
    if (arg1, arg2) trong _cache:
        trả về _cache[(arg1, arg2)]

    # Calculate giá trị
    kết quả = ... tính toán tốn kém ...
    _cache[(arg1, arg2)] = kết quả # Store dẫn đến bộ đệm
    kết quả trả về

Bạn có thể sử dụng biến toàn cục chứa từ điển thay vì giá trị mặc định; đó là vấn đề của hương vị.

Làm cách nào tôi có thể chuyển các tham số tùy chọn hoặc từ khóa từ hàm này sang hàm khác?

Thu thập các đối số bằng cách sử dụng bộ xác định *** trong danh sách tham số của hàm; điều này cung cấp cho bạn các đối số vị trí dưới dạng một bộ dữ liệu và các đối số từ khóa dưới dạng từ điển. Sau đó, bạn có thể chuyển các đối số này khi gọi một hàm khác bằng cách sử dụng ***:

def f(x, *args, **kwargs):
    ...
    kwargs['width'] = '14.3c'
    ...
    g(x, *args, **kwargs)

Sự khác biệt giữa đối số và tham số là gì?

Parameters được xác định bởi các tên xuất hiện trong định nghĩa hàm, trong khi arguments là các giá trị thực sự được truyền cho hàm khi gọi nó. Các tham số xác định những gì kind of arguments mà một hàm có thể chấp nhận. Ví dụ: đưa ra định nghĩa hàm

def func(foo, bar=None, **kwargs):
    vượt qua

foo, barkwargs là các thông số của func. Tuy nhiên, khi gọi func, ví dụ:

func(42, bar=314, extra=somevar)

các giá trị 42, 314somevar là các đối số.

Tại sao thay đổi danh sách 'y' cũng thay đổi danh sách 'x'?

Nếu bạn viết mã như:

>>> x = []
>>> y = x
>>> y.append(10)
>>> y
[10]
>>>x
[10]

bạn có thể thắc mắc tại sao việc thêm một phần tử vào y lại thay đổi x.

Có hai yếu tố tạo nên kết quả này:

  1. Các biến chỉ đơn giản là tên đề cập đến các đối tượng. Việc thực hiện y = x không tạo ra một bản sao của danh sách -- nó tạo ra một biến mới y tham chiếu đến cùng một đối tượng mà x tham chiếu đến. Điều này có nghĩa là chỉ có một đối tượng (danh sách) và cả xy đều tham chiếu đến nó.

  2. Danh sách là mutable, có nghĩa là bạn có thể thay đổi nội dung của chúng.

Sau lệnh gọi tới append(), nội dung của đối tượng có thể thay đổi đã thay đổi từ [] thành [10]. Vì cả hai biến đều tham chiếu đến cùng một đối tượng nên việc sử dụng một trong hai tên sẽ truy cập vào giá trị đã sửa đổi [10].

Thay vào đó, nếu chúng ta gán một đối tượng bất biến cho x:

>>> x = 5 # ints là bất biến
>>> y = x
>>> x = x + 1 # 5 không thể thay đổi được, chúng ta đang tạo một đối tượng mới ở đây
>>>x
6
>>> y
5

chúng ta có thể thấy rằng trong trường hợp này xy không còn bằng nhau nữa. Điều này là do các số nguyên là immutable và khi chúng ta thực hiện x = x + 1, chúng ta sẽ không thay đổi int 5 bằng cách tăng giá trị của nó; thay vào đó, chúng ta đang tạo một đối tượng mới (int 6) và gán nó cho x (nghĩa là thay đổi đối tượng mà x đề cập đến). Sau phép gán này, chúng ta có hai đối tượng (ints 65) và hai biến tham chiếu đến chúng (x hiện tham chiếu đến 6 nhưng y vẫn tham chiếu đến 5).

Một số thao tác (ví dụ y.append(10)y.sort()) làm thay đổi đối tượng, trong khi các thao tác bề ngoài tương tự (ví dụ y = y + [10]sorted(y)) tạo ra một đối tượng mới. Nói chung trong Python (và trong mọi trường hợp trong thư viện chuẩn), một phương thức làm thay đổi một đối tượng sẽ trả về None để giúp tránh nhầm lẫn giữa hai loại thao tác. Vì vậy, nếu bạn viết nhầm y.sort() vì nghĩ rằng nó sẽ cung cấp cho bạn một bản sao đã được sắp xếp của y, thì thay vào đó bạn sẽ nhận được None, điều này có thể khiến chương trình của bạn tạo ra một lỗi dễ dàng được chẩn đoán.

Tuy nhiên, có một lớp hoạt động trong đó cùng một hoạt động đôi khi có các hành vi khác nhau với các loại khác nhau: toán tử gán tăng cường. Ví dụ: += thay đổi danh sách nhưng không thay đổi bộ dữ liệu hoặc int (a_list += [1, 2, 3] tương đương với a_list.extend([1, 2, 3]) và thay đổi a_list, trong khi some_tuple += (1, 2, 3)some_int += 1 tạo đối tượng mới).

Nói cách khác:

  • Nếu chúng ta có một đối tượng có thể thay đổi (chẳng hạn như list, dict, set), chúng ta có thể sử dụng một số thao tác cụ thể để thay đổi nó và tất cả các biến tham chiếu đến nó sẽ thấy sự thay đổi.

  • Nếu chúng ta có một đối tượng bất biến (chẳng hạn như str, int, tuple), tất cả các biến tham chiếu đến nó sẽ luôn có cùng một giá trị, nhưng các thao tác biến đổi giá trị đó thành giá trị mới luôn trả về một đối tượng mới.

Nếu bạn muốn biết hai biến có tham chiếu đến cùng một đối tượng hay không, bạn có thể sử dụng toán tử is hoặc hàm id() tích hợp.

Làm cách nào để viết hàm có tham số đầu ra (gọi theo tham chiếu)?

Hãy nhớ rằng các đối số được truyền bằng phép gán trong Python. Vì phép gán chỉ tạo tham chiếu đến đối tượng nên không có bí danh nào giữa tên đối số trong lệnh gọi và hàm callee và do đó không có lệnh gọi theo tham chiếu. Bạn có thể đạt được hiệu quả mong muốn theo một số cách.

  1. Bằng cách trả về một bộ kết quả:

    >>> def func1(a, b):
    ... a = 'new-value' # a và b là tên địa phương
    ... b = b + 1 # assigned sang đối tượng mới
    ... trả về a, b # return giá trị mới
    ...
    >>> x, y = 'giá trị cũ', 99
    >>> func1(x, y)
    ('giá trị mới', 100)
    

    Đây gần như luôn là giải pháp rõ ràng nhất.

  2. Bằng cách sử dụng các biến toàn cục. Điều này không an toàn cho luồng và không được khuyến khích.

  3. Bằng cách truyền một đối tượng có thể thay đổi (có thể thay đổi tại chỗ):

    >>> def func2(a):
    ... a[0] = 'new-value' # 'a' tham chiếu đến danh sách có thể thay đổi
    ... a[1] = a[1] + 1 # changes một đối tượng được chia sẻ
    ...
    >>> args = ['giá trị cũ', 99]
    >>> func2(args)
    >>> lập luận
    ['giá trị mới', 100]
    
  4. Bằng cách chuyển vào một từ điển bị đột biến

    >>> def func3(args):
    ... args['a'] = 'new-value' # args là một từ điển có thể thay đổi
    ... args['b'] = args['b'] + 1 # change nó tại chỗ
    ...
    >>> args = {'a': 'giá trị cũ', 'b': 99}
    >>> func3(args)
    >>> lập luận
    {'a': 'giá trị mới', 'b': 100}
    
  5. Hoặc gộp các giá trị trong một thể hiện của lớp:

    >>> Không gian tên lớp:
    ... def __init__(self, /, **args):
    ... cho khóa, giá trị trong args.items():
    ... setattr(tự, khóa, giá trị)
    ...
    >>> def func4(args):
    ... args.a = 'new-value' # args là một Namespace có thể thay đổi
    ... args.b = args.b + 1 đối tượng # change tại chỗ
    ...
    >>> args = Không gian tên(a='old-value', b=99)
    >>> func4(args)
    >>> vars(args)
    {'a': 'giá trị mới', 'b': 100}
    

    Hầu như không bao giờ có lý do chính đáng để làm điều này trở nên phức tạp.

Lựa chọn tốt nhất của bạn là trả về một bộ chứa nhiều kết quả.

Làm cách nào để tạo hàm bậc cao hơn trong Python?

Bạn có hai lựa chọn: bạn có thể sử dụng phạm vi lồng nhau hoặc bạn có thể sử dụng các đối tượng có thể gọi được. Ví dụ: giả sử bạn muốn xác định linear(a,b) trả về hàm f(x) tính giá trị a*x+b. Sử dụng phạm vi lồng nhau:

def tuyến tính (a, b):
    kết quả chắc chắn (x):
        trả về a * x + b
    kết quả trả về

Hoặc sử dụng một đối tượng có thể gọi được

lớp tuyến tính:

    def __init__(self, a, b):
        self.a, self.b = a, b

    def __call__(self, x):
        trả về self.a * x + self.b

Trong cả hai trường hợp,

thuế = tuyến tính(0,3, 2)

đưa ra một đối tượng có thể gọi được trong đó taxes(10e6) == 0.3 * 10e6 + 2.

Cách tiếp cận đối tượng có thể gọi được có nhược điểm là chậm hơn một chút và dẫn đến mã dài hơn một chút. Tuy nhiên, lưu ý rằng một tập hợp các đối tượng có thể gọi được có thể chia sẻ chữ ký của chúng thông qua tính kế thừa

lớp  (tuyến tính):
    # __init__ kế thừa
    def __call__(self, x):
        return self.a * (x ** self.b)

Đối tượng có thể đóng gói trạng thái cho một số phương thức

quầy lớp:

    giá trị = 0

    tập def (tự, x):
        self.value = x

    def up(tự):
        self.value = self.value + 1

    def xuống (tự):
        self.value = self.value - 1

đếm = bộ đếm()
inc, dec, reset = count.up, count.down, count.set

Ở đây inc(), dec()reset() hoạt động giống như các hàm có chung biến đếm.

Làm cách nào để sao chép một đối tượng trong Python?

Nói chung, hãy thử copy.copy() hoặc copy.deepcopy() cho trường hợp chung. Không phải tất cả các đối tượng đều có thể được sao chép, nhưng hầu hết đều có thể.

Một số đối tượng có thể được sao chép dễ dàng hơn. Từ điển có phương thức copy()

newdict = olddict.copy()

Trình tự có thể được sao chép bằng cách cắt:

new_l = l[:]

Làm cách nào tôi có thể tìm thấy các phương thức hoặc thuộc tính của một đối tượng?

Đối với một phiên bản x của một lớp do người dùng định nghĩa, dir(x) trả về một danh sách các tên được sắp xếp theo thứ tự bảng chữ cái chứa các thuộc tính và phương thức của phiên bản cũng như các thuộc tính được xác định bởi lớp của nó.

Làm cách nào mã của tôi có thể khám phá tên của một đối tượng?

Nói chung là không thể, vì đồ vật thực sự không có tên. Về cơ bản, phép gán luôn gắn tên với một giá trị; điều tương tự cũng đúng với các câu lệnh defclass, nhưng trong trường hợp đó, giá trị có thể gọi được. Hãy xem xét đoạn mã sau:

>>> lớp A:
... vượt qua
...
>>> B = A
>>> a = B()
>>> b = a
>>> in(b)
<__main__.Một đối tượng tại 0x16D07CC>
>>> in(a)
<__main__.Một đối tượng tại 0x16D07CC>

Có thể cho rằng lớp này có một tên: mặc dù nó được gắn với hai tên và được gọi thông qua tên B nhưng phiên bản đã tạo vẫn được báo cáo là một phiên bản của lớp A. Tuy nhiên, không thể nói tên của phiên bản là a hay b vì cả hai tên đều bị ràng buộc với cùng một giá trị.

Nói chung, mã của bạn không cần thiết phải "biết tên" của các giá trị cụ thể. Trừ khi bạn cố tình viết các chương trình nội tâm, đây thường là dấu hiệu cho thấy sự thay đổi cách tiếp cận có thể có lợi.

Trong comp.lang.python, Fredrik Lundh đã từng đưa ra một câu trả lời tuyệt vời cho câu hỏi này:

Giống như cách bạn lấy tên của con mèo mà bạn tìm thấy trước hiên nhà: bản thân con mèo (vật thể) không thể cho bạn biết tên của nó và nó không thực sự quan tâm -- vì vậy cách duy nhất để biết nó được gọi là gì là hỏi tất cả hàng xóm của bạn (không gian tên) xem đó có phải là con mèo (vật thể) của họ không...

....và đừng ngạc nhiên nếu bạn thấy rằng nó được biết đến với nhiều cái tên, hoặc không có tên nào cả!

Có chuyện gì với quyền ưu tiên của toán tử dấu phẩy?

Dấu phẩy không phải là toán tử trong Python. Hãy xem xét phiên này:

>>> "a" trong "b", "a"
(Sai, 'a')

Vì dấu phẩy không phải là toán tử mà là dấu phân cách giữa các biểu thức nên giá trị ở trên được đánh giá như thể bạn đã nhập:

("a" trong "b"), "a"

không:

"a" trong ("b", "a")

Điều tương tự cũng đúng với các toán tử gán khác nhau (=, +=, v.v.). Chúng không thực sự là các toán tử mà là các dấu phân cách cú pháp trong câu lệnh gán.

Có toán tử tương đương với toán tử ternary "?:" của C không?

Vâng, có. Cú pháp như sau:

[on_true] nếu [biểu thức] khác [on_false]

x, y = 50, 25
nhỏ = x nếu x < y khác y

Trước khi cú pháp này được giới thiệu trong Python 2.5, một thành ngữ phổ biến là sử dụng các toán tử logic

[biểu thức]  [on_true] hoặc [on_false]

Tuy nhiên, thành ngữ này không an toàn vì nó có thể cho kết quả sai khi on_true có giá trị boolean sai. Vì vậy, tốt hơn hết bạn nên sử dụng mẫu ... if ... else ....

Có thể viết các dòng một cách khó hiểu bằng Python không?

Vâng. Thông thường việc này được thực hiện bằng cách lồng lambda vào trong lambda. Xem ba ví dụ sau, được điều chỉnh một chút từ Ulf Bartelt:

từ nhập functools giảm

# Primes < 1000
print(list(filter(Không ,map(lambda y:y*reduce(lambda x,y:x*y!=0,
map(lambda x,y=y:y%x,range(2,int(pow(y,0.5)+1))),1),range(2,1000)))))

# First 10 số Fibonacci
print(list(map(lambda x,f=lambda x,f:(f(x-1,f)+f(x-2,f)) if x>1 else 1:
f(x,f), phạm vi(10))))

bộ # Mandelbrot
print((lambda Ru,Ro,Iu,Io,IM,Sx,Sy:reduce(lambda x,y:x+'\n'+y,map(lambda y,
Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,Sy=Sy,L=lambda yc,Iu=Iu,Io=Io,Ru=Ru,Ro=Ro,i=IM,
Sx=Sx,Sy=Sy:reduce(lambda x,y:x+y,map(lambda x,xc=Ru,yc=yc,Ru=Ru,Ro=Ro,
i=i,Sx=Sx,F=lambda xc,yc,x,y,k,f=lambda xc,yc,x,y,k,f:(k<=0)hoặc (x*x+y*y
>=4.0) hoặc 1+f(xc,yc,x*x-y*y+xc,2.0*x*y+yc,k-1,f):f(xc,yc,x,y,k,f):chr(
64+F(Ru+x*(Ro-Ru)/Sx,yc,0,0,i)),range(Sx))):L(Iu+y*(Io-Iu)/Sy), phạm vi(Sy
))))(-2.1, 0.7, -1.2, 1.2, 30, 80, 24))
# \___ ___/ \___ ___/ |   | |__ dòng trên màn hình
#        V V |   |______ cột trên màn hình
#        zz007zz |__________ tối đa "lặp lại"
phạm vi #        zz008zz_________________ trên trục y
# |____________________________ phạm vi trên trục x

Đừng thử điều này ở nhà, trẻ em!

Dấu gạch chéo (/) trong danh sách tham số của hàm có ý nghĩa gì?

Dấu gạch chéo trong danh sách đối số của hàm biểu thị rằng các tham số trước nó chỉ mang tính vị trí. Các tham số chỉ có vị trí là những tham số không có tên có thể sử dụng được bên ngoài. Khi gọi một hàm chỉ chấp nhận các tham số vị trí, các đối số sẽ được ánh xạ tới các tham số chỉ dựa trên vị trí của chúng. Ví dụ: divmod() là một hàm chỉ chấp nhận các tham số vị trí. Tài liệu của nó trông như thế này:

>>> trợ giúp(divmod)
Trợ giúp về hàm divmod tích hợp trong nội dung mô-đun:

divmod(x, y, /)
    Trả về bộ dữ liệu (x//y, x%y).  Bất biến: div*y + mod == x.

Dấu gạch chéo ở cuối danh sách tham số có nghĩa là cả hai tham số chỉ có vị trí. Do đó, việc gọi divmod() bằng các đối số từ khóa sẽ dẫn đến lỗi

>>> divmod(x=3, y=4)
Traceback (cuộc gọi gần đây nhất):
  Tệp "<stdin>", dòng 1, trong <module>
TypeError: divmod() không có đối số từ khóa

Số và chuỗi

Làm cách nào để chỉ định số nguyên thập lục phân và số bát phân?

Để chỉ định một chữ số bát phân, đặt trước giá trị bát phân bằng số 0, sau đó là chữ "o" viết thường hoặc viết hoa. Ví dụ: để đặt biến "a" thành giá trị bát phân "10" (8 ở dạng thập phân), hãy nhập:

>>> a = 0o10
>>> một
8

Hệ thập lục phân cũng dễ dàng như vậy. Chỉ cần đặt trước số thập lục phân bằng số 0, sau đó là chữ "x" viết thường hoặc viết hoa. Các chữ số thập lục phân có thể được chỉ định bằng chữ thường hoặc chữ hoa. Ví dụ: trong trình thông dịch Python:

>>> a = 0xa5
>>> một
165
>>> b = 0XB2
>>>b
178

Tại sao -22 // 10 trả về -3?

Nó chủ yếu được thúc đẩy bởi mong muốn rằng i % j có cùng dấu hiệu với j. Nếu bạn muốn điều đó và cũng muốn

tôi == (i // j) * j + (i % j)

thì phép chia số nguyên phải trả về giá trị sàn. C cũng yêu cầu giữ danh tính đó và sau đó các trình biên dịch cắt bớt i // j cần làm cho i % j có cùng dấu với i.

Có rất ít trường hợp sử dụng thực tế cho i % j khi j âm. Khi j dương thì có rất nhiều và trong hầu hết tất cả chúng, việc i % j>= 0 sẽ hữu ích hơn. Nếu đồng hồ chỉ 10 giờ thì 200 giờ trước nó nói gì? -190 % 12 == 2 rất hữu ích; -190 % 12 == -10 là một con bọ đang chờ cắn.

Làm cách nào để có được thuộc tính int bằng chữ thay vì SyntaxError?

Cố gắng tra cứu thuộc tính chữ int theo cách thông thường sẽ cho ra SyntaxError vì dấu chấm được coi là dấu thập phân:

>>> 1.__lớp__
  Tệp "<stdin>", dòng 1
  1.__lớp__
   ^
Lỗi Cú pháp: chữ thập phân không hợp lệ

Giải pháp là tách chữ khỏi dấu chấm bằng dấu cách hoặc dấu ngoặc đơn.

>>> 1 .__class__
<class 'int'>
>>> (1).__class__
<class 'int'>

Làm cách nào để chuyển đổi một chuỗi thành một số?

Đối với số nguyên, hãy sử dụng hàm tạo kiểu int() tích hợp sẵn, ví dụ: int('144') == 144. Tương tự, float() chuyển đổi thành số dấu phẩy động, ví dụ: float('144') == 144.0.

Theo mặc định, các giá trị này diễn giải số dưới dạng thập phân, do đó int('0144') == 144 giữ giá trị đúng và int('0x144') tăng ValueError. int(string, base) lấy cơ sở để chuyển đổi làm đối số tùy chọn thứ hai, vì vậy int( '0x144', 16) == 324. Nếu cơ số được chỉ định là 0 thì số đó sẽ được diễn giải bằng các quy tắc của Python: '0o' ở đầu biểu thị số bát phân và '0x' biểu thị số thập lục phân.

Không sử dụng hàm eval() tích hợp nếu tất cả những gì bạn cần là chuyển đổi chuỗi thành số. eval() sẽ chậm hơn đáng kể và gây ra rủi ro bảo mật: ai đó có thể chuyển cho bạn một biểu thức Python có thể gây ra tác dụng phụ không mong muốn. Ví dụ: ai đó có thể vượt qua __import__('os').system("rm -rf $HOME") để xóa thư mục chính của bạn.

eval() còn có tác dụng diễn giải các số dưới dạng biểu thức Python, do đó, chẳng hạn, eval('09') đưa ra lỗi cú pháp vì Python không cho phép '0' đứng đầu trong một số thập phân (ngoại trừ '0').

Làm cách nào để chuyển đổi một số thành một chuỗi?

Ví dụ: để chuyển đổi số 144 thành chuỗi '144', hãy sử dụng hàm tạo kiểu str() có sẵn. Nếu bạn muốn biểu diễn theo hệ thập lục phân hoặc bát phân, hãy sử dụng các hàm dựng sẵn hex() hoặc oct(). Để biết định dạng ưa thích, hãy xem phần dây fCú pháp định dạng chuỗi. Ví dụ: "{:04d}".format(144) mang lại '0144'"{:.3f}".format(1.0/3.0) mang lại '0.333'.

Làm cách nào để sửa đổi một chuỗi tại chỗ?

Bạn không thể, vì chuỗi là bất biến. Trong hầu hết các trường hợp, bạn chỉ cần tạo một chuỗi mới từ các phần khác nhau mà bạn muốn tập hợp nó lại. Tuy nhiên, nếu bạn cần một đối tượng có khả năng sửa đổi dữ liệu Unicode tại chỗ, hãy thử sử dụng đối tượng io.StringIO hoặc mô-đun array

>>> nhập io
>>> s = "Xin chào thế giới"
>>> sio = io.StringIO(s)
>>> sio.getvalue()
'Xin chào thế giới'
>>> sio.seek(7)
7
>>> sio.write("ở đó!")
6
>>> sio.getvalue()
'Xin chào, đằng kia!'

>>> nhập mảng
>>> a = array.array('w', s)
>>> in(a)
mảng('w', 'Xin chào thế giới')
>>> a[0] = 'y'
>>> in(a)
mảng('w', 'yello, thế giới')
>>> a.tounicode()
'xin chào thế giới'

Làm cách nào để sử dụng chuỗi để gọi hàm/phương thức?

Có nhiều kỹ thuật khác nhau.

  • Cách tốt nhất là sử dụng từ điển ánh xạ các chuỗi thành các hàm. Ưu điểm chính của kỹ thuật này là các chuỗi không cần phải khớp với tên của hàm. Đây cũng là kỹ thuật chính được sử dụng để mô phỏng cấu trúc trường hợp:

    chắc chắn a():
        vượt qua
    
    chắc chắn b():
        vượt qua
    
    công văn = {'go': a, 'stop': b} # Note thiếu parens cho funcs
    
    công văn [get_input()]() # Note dấu ngoặc đơn để gọi hàm
    
  • Sử dụng chức năng tích hợp getattr():

    nhập khẩu foo
    getattr(foo, 'bar')()
    

    Lưu ý rằng getattr() hoạt động trên mọi đối tượng, bao gồm các lớp, phiên bản lớp, mô-đun, v.v.

    Điều này được sử dụng ở một số nơi trong thư viện tiêu chuẩn, như thế này

    lớp Foo:
        def do_foo(tự):
            ...
    
        def do_bar(tự):
            ...
    
    f = getattr(foo_instance, 'do_' + opname)
    f()
    
  • Sử dụng locals() để phân giải tên hàm:

    định nghĩa myFunc():
        in ("xin chào")
    
    fname = "myFunc"
    
    f = người địa phương()[fname]
    f()
    

Có tương đương với chomp() của Perl để xóa các dòng mới ở cuối chuỗi không?

Bạn có thể sử dụng S.rstrip("\r\n") để xóa tất cả các lần xuất hiện của bất kỳ dấu kết thúc dòng nào khỏi cuối chuỗi S mà không xóa các khoảng trắng ở cuối khác. Nếu chuỗi S đại diện cho nhiều hơn một dòng, với một số dòng trống ở cuối, thì dấu kết thúc dòng cho tất cả các dòng trống sẽ bị xóa:

>>> dòng = ("dòng 1 \r\n"
... "\r\n"
... "\r\n")
>>> dòng.rstrip("\n\r")
'dòng 1'

Vì điều này thường chỉ được mong muốn khi đọc từng dòng một văn bản nên sử dụng S.rstrip() theo cách này sẽ hoạt động tốt.

Có tương đương với scanf() hoặc sscanf() không?

Không phải như vậy.

Để phân tích cú pháp đầu vào đơn giản, cách tiếp cận dễ dàng nhất thường là chia dòng thành các từ được phân tách bằng khoảng trắng bằng phương pháp split() của các đối tượng chuỗi, sau đó chuyển đổi chuỗi thập phân thành giá trị số bằng int() hoặc float(). split() hỗ trợ tham số "sep" tùy chọn rất hữu ích nếu dòng sử dụng thứ gì đó không phải khoảng trắng làm dấu phân cách.

Để phân tích cú pháp đầu vào phức tạp hơn, biểu thức chính quy mạnh hơn sscanf của C và phù hợp hơn với tác vụ.

Lỗi UnicodeDecodeError hoặc UnicodeEncodeError nghĩa là gì?

Xem Unicode HOWTO.

Tôi có thể kết thúc chuỗi thô bằng số dấu gạch chéo ngược lẻ không?

Một chuỗi thô kết thúc bằng số lẻ dấu gạch chéo ngược sẽ thoát khỏi dấu ngoặc kép của chuỗi:

>>> r'C:\this\will\not\work\'
  Tệp "<stdin>", dòng 1
    r'C:\this\will\not\work\'
    ^
Lỗi cú pháp: chuỗi ký tự bị kết thúc (được phát hiện ở dòng 1)

Có một số cách giải quyết cho việc này. Một là sử dụng các chuỗi thông thường và nhân đôi dấu gạch chéo ngược

>>> 'C:\\this\\will\\work\\'
'C:\\this\\will\\work\\'

Một cách khác là nối một chuỗi thông thường chứa dấu gạch chéo ngược thoát thành chuỗi thô:

>>> r'C:\this\will\work' '\\'
'C:\\this\\will\\work\\'

Cũng có thể sử dụng os.path.join() để thêm dấu gạch chéo ngược trên Windows

>>> os.path.join(r'C:\this\will\work', '')
'C:\\this\\will\\work\\'

Lưu ý rằng mặc dù dấu gạch chéo ngược sẽ "thoát" khỏi trích dẫn nhằm mục đích xác định vị trí kết thúc của chuỗi thô, nhưng không xảy ra hiện tượng thoát khi diễn giải giá trị của chuỗi thô. Nghĩa là, dấu gạch chéo ngược vẫn hiện diện trong giá trị của chuỗi thô:

>>> r'dấu gạch chéo ngược\'được bảo tồn'
"dấu gạch chéo ngược\\'được giữ nguyên"

Đồng thời xem thông số kỹ thuật trong language reference.

Hiệu suất

Chương trình của tôi quá chậm. Làm thế nào để tôi tăng tốc nó?

Nói chung đó là một điều khó khăn. Đầu tiên, đây là danh sách những điều cần nhớ trước khi đi sâu hơn:

  • Đặc điểm hiệu suất khác nhau tùy theo cách triển khai Python. Zz001zz này tập trung vào CPython.

  • Hành vi có thể khác nhau giữa các hệ điều hành, đặc biệt khi nói về I/O hoặc đa luồng.

  • Bạn phải luôn tìm thấy các điểm nóng trong chương trình before của mình để cố gắng tối ưu hóa bất kỳ mã nào (xem mô-đun profile).

  • Viết tập lệnh điểm chuẩn sẽ cho phép bạn lặp lại nhanh chóng khi tìm kiếm các cải tiến (xem mô-đun timeit).

  • Chúng tôi khuyên bạn nên có phạm vi bao phủ mã tốt (thông qua thử nghiệm đơn vị hoặc bất kỳ kỹ thuật nào khác) trước khi có khả năng đưa ra các hồi quy ẩn trong các tối ưu hóa phức tạp.

Nói như vậy, có rất nhiều thủ thuật để tăng tốc mã Python. Dưới đây là một số nguyên tắc chung giúp bạn đạt được mức hiệu suất chấp nhận được:

  • Làm cho thuật toán của bạn nhanh hơn (hoặc thay đổi sang thuật toán nhanh hơn) có thể mang lại lợi ích lớn hơn nhiều so với việc cố gắng áp dụng các thủ thuật tối ưu hóa vi mô trên toàn bộ mã của bạn.

  • Sử dụng cấu trúc dữ liệu phù hợp. Tài liệu nghiên cứu về mô-đun Các loại tích hợpcollections.

  • Khi thư viện tiêu chuẩn cung cấp một nguyên tắc cơ bản để thực hiện một điều gì đó, nó có khả năng (mặc dù không được đảm bảo) sẽ nhanh hơn bất kỳ giải pháp thay thế nào mà bạn có thể nghĩ ra. Điều này đúng gấp đôi đối với các dạng nguyên thủy được viết bằng C, chẳng hạn như nội dung và một số loại phần mở rộng. Ví dụ: hãy đảm bảo sử dụng phương thức tích hợp sẵn list.sort() hoặc hàm sorted() có liên quan để thực hiện sắp xếp (và xem Kỹ thuật sắp xếp để biết ví dụ về cách sử dụng ở mức độ vừa phải).

  • Sự trừu tượng có xu hướng tạo ra sự gián tiếp và buộc người phiên dịch phải làm việc nhiều hơn. Nếu mức độ gián tiếp lớn hơn số lượng công việc hữu ích đã thực hiện thì chương trình của bạn sẽ chậm hơn. Bạn nên tránh sự trừu tượng hóa quá mức, đặc biệt là dưới dạng các hàm hoặc phương thức nhỏ (thường gây bất lợi cho khả năng đọc).

Nếu bạn đã đạt đến giới hạn mà Python thuần túy có thể cho phép, thì có những công cụ sẽ đưa bạn đi xa hơn. Ví dụ: Cython có thể biên dịch phiên bản mã Python được sửa đổi một chút thành phần mở rộng C và có thể được sử dụng trên nhiều nền tảng khác nhau. Cython có thể tận dụng khả năng biên dịch (và chú thích loại tùy chọn) để làm cho mã của bạn nhanh hơn đáng kể so với khi được thông dịch. Nếu bạn tự tin vào kỹ năng lập trình C của mình thì bạn cũng có thể tự mình write a C extension module.

Xem thêm

Trang wiki dành cho performance tips.

Cách hiệu quả nhất để nối nhiều chuỗi lại với nhau là gì?

Các đối tượng strbytes là bất biến, do đó việc ghép nhiều chuỗi lại với nhau sẽ không hiệu quả vì mỗi phép nối sẽ tạo ra một đối tượng mới. Trong trường hợp chung, tổng chi phí thời gian chạy là bậc hai của tổng chiều dài chuỗi.

Để tích lũy nhiều đối tượng str, thành ngữ được khuyến nghị là đặt chúng vào một danh sách và gọi str.join() ở cuối

khối = []
cho s trong my_strings:
    chunks.append(s)
result = ''.join(chunks)

(Một thành ngữ khá hiệu quả khác là sử dụng io.StringIO.)

Để tích lũy nhiều đối tượng bytes, thành ngữ được khuyến nghị là mở rộng đối tượng bytearray bằng cách sử dụng phép nối tại chỗ (toán tử +=):

kết quả = bytearray()
cho b trong my_bytes_objects:
    kết quả += b

Trình tự (bộ dữ liệu/danh sách)

Làm cách nào để chuyển đổi giữa các bộ dữ liệu và danh sách?

Hàm tạo kiểu tuple(seq) chuyển đổi bất kỳ chuỗi nào (thực tế là bất kỳ chuỗi lặp nào) thành một bộ dữ liệu có cùng các mục theo cùng một thứ tự.

Ví dụ: tuple([1, 2, 3]) mang lại (1, 2, 3)tuple('abc') mang lại ('a', 'b', 'c'). Nếu đối số là một bộ dữ liệu, nó sẽ không tạo một bản sao mà trả về cùng một đối tượng, do đó, sẽ rẻ hơn khi gọi tuple() khi bạn không chắc chắn rằng một đối tượng đã là một bộ dữ liệu hay chưa.

Hàm tạo kiểu list(seq) chuyển đổi bất kỳ chuỗi hoặc có thể lặp nào thành một danh sách có cùng các mục theo cùng một thứ tự. Ví dụ: list((1, 2, 3)) mang lại [1, 2, 3]list('abc') mang lại ['a', 'b', 'c']. Nếu đối số là một danh sách, nó sẽ tạo một bản sao giống như seq[:].

Chỉ số tiêu cực là gì?

Chuỗi Python được lập chỉ mục bằng số dương và số âm. Đối với số dương 0 là chỉ số đầu tiên 1 là chỉ số thứ hai, v.v. Đối với các chỉ số âm -1 là chỉ số cuối cùng và -2 là chỉ số áp chót (cạnh cuối cùng), v.v. Hãy nghĩ seq[-n] giống như seq[len(seq)-n].

Sử dụng chỉ số tiêu cực có thể rất thuận tiện. Ví dụ: S[:-1] là toàn bộ chuỗi ngoại trừ ký tự cuối cùng của nó, rất hữu ích để xóa dấu dòng mới ở cuối chuỗi.

Làm cách nào để lặp lại một chuỗi theo thứ tự ngược lại?

Sử dụng chức năng tích hợp reversed():

cho x đảo ngược(trình tự):
    ... # do cái gì đó với x ...

Điều này sẽ không chạm vào trình tự ban đầu của bạn mà tạo một bản sao mới với thứ tự đảo ngược để lặp lại.

Làm thế nào để bạn loại bỏ các bản sao khỏi danh sách?

Xem Sách dạy nấu ăn Python để biết phần thảo luận dài về nhiều cách thực hiện việc này:

Nếu bạn không ngại sắp xếp lại danh sách, hãy sắp xếp nó rồi quét từ cuối danh sách, xóa các bản sao khi bạn thực hiện:

nếu danh sách của tôi:
    mylist.sort()
    cuối cùng = danh sách của tôi [-1]
    cho i trong phạm vi(len(mylist)-2, -1, -1):
        nếu cuối cùng == mylist[i]:
            del danh sách của tôi[i]
        khác:
            cuối cùng = danh sách của tôi [i]

Nếu tất cả các thành phần của danh sách có thể được sử dụng làm khóa cài đặt (nghĩa là tất cả chúng đều là hashable) thì điều này thường nhanh hơn

danh sách của tôi = danh sách (bộ (danh sách của tôi))

Thao tác này sẽ chuyển danh sách thành một tập hợp, từ đó loại bỏ các bản sao và sau đó quay lại danh sách.

Làm cách nào để xóa nhiều mục khỏi danh sách?

Giống như việc loại bỏ các bản sao, việc lặp lại ngược lại một cách rõ ràng với điều kiện xóa là một khả năng. Tuy nhiên, sẽ dễ dàng và nhanh hơn khi sử dụng thay thế lát cắt bằng phép lặp chuyển tiếp ẩn hoặc rõ ràng. Dưới đây là ba biến thể:

mylist[:] = filter(keep_function, mylist)
mylist[:] = (x cho x trong mylist nếu keep_condition)
mylist[:] = [x cho x trong mylist nếu keep_condition]

Việc hiểu danh sách có thể nhanh nhất.

Làm thế nào để bạn tạo một mảng trong Python?

Sử dụng danh sách:

["cái này", 1, "là", "một", "mảng"]

Các danh sách tương đương với mảng C hoặc Pascal về độ phức tạp về thời gian; điểm khác biệt chính là danh sách Python có thể chứa các đối tượng thuộc nhiều loại khác nhau.

Mô-đun array cũng cung cấp các phương thức tạo mảng có kiểu cố định với cách biểu diễn nhỏ gọn, nhưng chúng lập chỉ mục chậm hơn so với danh sách. Cũng lưu ý rằng NumPy và các gói bên thứ ba khác cũng xác định cấu trúc dạng mảng với nhiều đặc điểm khác nhau.

Để có được danh sách liên kết kiểu Lisp, bạn có thể mô phỏng cons cells bằng cách sử dụng bộ dữ liệu

lisp_list = ("thích", ("cái này", ("ví dụ", Không ) ) )

Nếu muốn có khả năng thay đổi, bạn có thể sử dụng danh sách thay vì bộ dữ liệu. Ở đây, phần tương tự của Lisp carlisp_list[0] và phần tương tự của cdrlisp_list[1]. Chỉ thực hiện việc này nếu bạn chắc chắn mình thực sự cần, vì cách này thường chậm hơn rất nhiều so với việc sử dụng danh sách Python.

Làm cách nào để tạo danh sách đa chiều?

Có thể bạn đã thử tạo một mảng đa chiều như thế này:

>>> A = [[Không ] * 2] * 3

Điều này có vẻ đúng nếu bạn in nó:

>>> A
[[Không, Không], [Không, Không], [Không, Không]]

Nhưng khi bạn gán một giá trị, nó sẽ hiển thị ở nhiều nơi:

>>> A[0][0] = 5
>>> A
[[5, Không có], [5, Không có], [5, Không có]]

Lý do là việc sao chép danh sách bằng * không tạo ra bản sao mà chỉ tạo các tham chiếu đến các đối tượng hiện có. Zz001zz tạo một danh sách chứa 3 tham chiếu đến cùng một danh sách có độ dài hai. Những thay đổi đối với một hàng sẽ hiển thị ở tất cả các hàng, gần như chắc chắn đó không phải là điều bạn muốn.

Cách tiếp cận được đề xuất là trước tiên hãy tạo danh sách có độ dài mong muốn, sau đó điền vào từng phần tử bằng danh sách mới tạo

A = [Không ] * 3
cho tôi trong phạm vi (3):
    A[i] = [Không ] * 2

Điều này tạo ra một danh sách chứa 3 danh sách khác nhau có độ dài hai. Bạn cũng có thể sử dụng tính năng hiểu danh sách:

w, h = 2, 3
A = [[Không ] * w với i trong phạm vi (h)]

Hoặc, bạn có thể sử dụng tiện ích mở rộng cung cấp kiểu dữ liệu ma trận; NumPy được biết đến nhiều nhất.

Làm cách nào để áp dụng một phương thức hoặc hàm cho một chuỗi đối tượng?

Để gọi một phương thức hoặc hàm và tích lũy các giá trị trả về trong danh sách, list comprehension là một giải pháp hay:

result = [obj.method() cho obj trong danh sách của tôi]

result = [function(obj) cho obj trong danh sách của tôi]

Để chỉ chạy phương thức hoặc hàm mà không lưu giá trị trả về, một vòng lặp for đơn giản sẽ đủ

cho obj trong danh sách của tôi:
    obj.method()

cho obj trong danh sách của tôi:
    hàm(obj)

Tại sao a_tuple[i] += ['item'] lại đưa ra ngoại lệ khi tính năng bổ sung hoạt động?

Điều này là do sự kết hợp giữa thực tế là các toán tử gán tăng cường là các toán tử assignment và sự khác biệt giữa các đối tượng có thể thay đổi và không thể thay đổi trong Python.

Cuộc thảo luận này áp dụng chung khi các toán tử gán tăng cường được áp dụng cho các phần tử của bộ dữ liệu trỏ đến các đối tượng có thể thay đổi, nhưng chúng tôi sẽ sử dụng list+= làm ví dụ.

Nếu bạn đã viết:

>>> a_tuple = (1, 2)
>>> a_tuple[0] += 1
Traceback (cuộc gọi gần đây nhất):
   ...
TypeError: đối tượng 'tuple' không hỗ trợ gán mục

Lý do cho ngoại lệ phải rõ ràng ngay lập tức: 1 được thêm vào đối tượng a_tuple[0] trỏ đến (1), tạo ra đối tượng kết quả, 2, nhưng khi chúng tôi cố gắng gán kết quả tính toán, 2, cho phần tử 0 của bộ dữ liệu, chúng tôi gặp lỗi vì chúng tôi không thể thay đổi thành phần nào của bộ dữ liệu trỏ tới.

Dưới phần trình bày, những gì câu lệnh gán tăng cường này đang thực hiện gần như thế này

>>> kết quả = a_tuple[0] + 1
>>> a_tuple[0] = kết quả
Traceback (cuộc gọi gần đây nhất):
  ...
TypeError: đối tượng 'tuple' không hỗ trợ gán mục

Chính phần gán của thao tác sẽ tạo ra lỗi vì bộ dữ liệu là không thể thay đổi.

Khi bạn viết một cái gì đó như:

>>> a_tuple = (['foo'], 'bar')
>>> a_tuple[0] += ['item']
Traceback (cuộc gọi gần đây nhất):
  ...
TypeError: đối tượng 'tuple' không hỗ trợ gán mục

Ngoại lệ đáng ngạc nhiên hơn một chút và thậm chí còn đáng ngạc nhiên hơn là mặc dù có lỗi nhưng phần bổ sung vẫn hoạt động

>>> a_tuple[0]
['foo', 'mục']

Để biết lý do tại sao điều này xảy ra, bạn cần biết rằng (a) nếu một đối tượng triển khai phương thức ma thuật __iadd__(), nó sẽ được gọi khi phép gán tăng cường += được thực thi và giá trị trả về của nó là giá trị được sử dụng trong câu lệnh gán; và (b) đối với danh sách, __iadd__() tương đương với việc gọi extend() trong danh sách và trả về danh sách. Đó là lý do tại sao chúng tôi nói rằng đối với danh sách, += là "viết tắt" của list.extend():

>>> a_list = []
>>> a_list += [1]
>>> a_list
[1]

Điều này tương đương với:

>>> kết quả = a_list.__iadd__([1])
>>> a_list = kết quả

Đối tượng được trỏ tới bởi a_list đã bị thay đổi và con trỏ tới đối tượng bị đột biến được gán lại cho a_list. Kết quả cuối cùng của phép gán là không hoạt động, vì nó là một con trỏ tới cùng một đối tượng mà a_list đã trỏ tới trước đó, nhưng phép gán vẫn diễn ra.

Vì vậy, trong ví dụ về bộ dữ liệu của chúng tôi, những gì đang xảy ra tương đương với:

>>> kết quả = a_tuple[0].__iadd__(['item'])
>>> a_tuple[0] = kết quả
Traceback (cuộc gọi gần đây nhất):
  ...
TypeError: đối tượng 'tuple' không hỗ trợ gán mục

__iadd__() thành công và do đó danh sách được mở rộng, nhưng mặc dù result trỏ đến cùng một đối tượng mà a_tuple[0] đã trỏ tới, phép gán cuối cùng đó vẫn dẫn đến lỗi, vì các bộ dữ liệu là bất biến.

Tôi muốn thực hiện một thao tác sắp xếp phức tạp: bạn có thể thực hiện Biến đổi Schwartzian trong Python không?

Kỹ thuật này do Randal Schwartz của cộng đồng Perl thực hiện, sắp xếp các phần tử của danh sách theo số liệu ánh xạ từng phần tử tới "giá trị sắp xếp" của nó. Trong Python, sử dụng đối số key cho phương thức list.sort():

Đã sắp xếp = L[:]
Isorted.sort(key=lambda s: int(s[10:15]))

Làm cách nào tôi có thể sắp xếp một danh sách theo giá trị từ danh sách khác?

Hợp nhất chúng thành một bộ lặp gồm các bộ dữ liệu, sắp xếp danh sách kết quả và sau đó chọn phần tử bạn muốn.

>>> list1 = ["what", "I'm", "sorting", "by"]
>>> list2 = ["something", "else", "to", "sort"]
>>> pairs = zip(list1, list2)
>>> pairs = sorted(pairs)
>>> pairs
[("I'm", 'else'), ('by', 'sort'), ('sorting', 'to'), ('what', 'something')]
>>> result = [x[1] for x in pairs]
>>> result
['else', 'sort', 'to', 'something']

Đối tượng

Một lớp học là gì?

Một lớp là loại đối tượng cụ thể được tạo bằng cách thực thi một câu lệnh lớp. Các đối tượng lớp được sử dụng làm mẫu để tạo các đối tượng cá thể, bao gồm cả dữ liệu (thuộc tính) và mã (phương thức) dành riêng cho một kiểu dữ liệu.

Một lớp có thể dựa trên một hoặc nhiều lớp khác, được gọi là (các) lớp cơ sở của nó. Sau đó nó kế thừa các thuộc tính và phương thức của các lớp cơ sở. Điều này cho phép một mô hình đối tượng được tinh chỉnh liên tục bằng tính kế thừa. Bạn có thể có một lớp Mailbox chung cung cấp các phương thức truy cập cơ bản cho hộp thư và các lớp con như MboxMailbox, MaildirMailbox, OutlookMailbox xử lý các định dạng hộp thư cụ thể khác nhau.

Phương pháp là gì?

Một phương thức là một hàm trên một số đối tượng x mà bạn thường gọi là x.name(arguments...). Các phương thức được định nghĩa là các hàm bên trong định nghĩa lớp

lớp C:
    def meth(tự, arg):
        trả về arg * 2 + self.attribute

Bản thân là gì?

Self chỉ là tên quy ước cho đối số đầu tiên của một phương thức. Một phương thức được định nghĩa là meth(self, a, b, c) nên được gọi là x.meth(a, b, c) đối với một số trường hợp x của lớp mà định nghĩa xảy ra; phương thức được gọi sẽ nghĩ nó được gọi là meth(x, a, b, c).

Xem thêm Tại sao 'bản thân' phải được sử dụng rõ ràng trong các định nghĩa và lệnh gọi phương thức?.

Làm cách nào để kiểm tra xem một đối tượng là một thể hiện của một lớp nhất định hay của một lớp con của nó?

Sử dụng chức năng tích hợp isinstance(obj, cls). Bạn có thể kiểm tra xem một đối tượng có phải là phiên bản của bất kỳ lớp nào hay không bằng cách cung cấp một bộ dữ liệu thay vì một lớp duy nhất, ví dụ: isinstance(obj, (class1, class2, ...)) và cũng có thể kiểm tra xem một đối tượng có phải là một trong các loại có sẵn của Python hay không, ví dụ: isinstance(obj, str) hoặc isinstance(obj, (int, float, complex)).

Lưu ý rằng isinstance() cũng kiểm tra tính kế thừa ảo từ abstract base class. Vì vậy, bài kiểm tra sẽ trả về True cho một lớp đã đăng ký ngay cả khi chưa được kế thừa trực tiếp hoặc gián tiếp từ lớp đó. Để kiểm tra "kế thừa thực sự", hãy quét method resolution order (MRO) của lớp:

từ bộ sưu tập.abc nhập bản đồ

lớp P:
     vượt qua

lớp C(P):
    vượt qua

Ánh xạ.register(P)
>>> c = C()
>>> isinstance(c, C) # direct
đúng
>>> isinstance(c, P) # indirect
đúng
>>> isinstance(c, Ánh xạ) # virtual
đúng

chuỗi kế thừa # Actual
>>> (c).__mro__
(<lớp 'C'>, <lớp 'P'>, <lớp 'đối tượng'>)

# Test cho "thừa kế thực sự"
>>> Ánh xạ theo kiểu(c).__mro__
sai

Lưu ý rằng hầu hết các chương trình không thường xuyên sử dụng isinstance() trên các lớp do người dùng xác định. Nếu bạn đang tự mình phát triển các lớp, một phong cách hướng đối tượng phù hợp hơn là xác định các phương thức trên các lớp gói gọn một hành vi cụ thể, thay vì kiểm tra lớp của đối tượng và thực hiện một việc khác dựa trên lớp đó là gì. Ví dụ: nếu bạn có một hàm thực hiện điều gì đó

tìm kiếm def (obj):
    nếu isinstance(obj, Hộp thư):
        ... # code để tìm kiếm hộp thư
    Elif isinstance(obj, Tài liệu):
        ... # code để tìm kiếm tài liệu
    Elif ...

Cách tiếp cận tốt hơn là xác định phương thức search() trên tất cả các lớp và chỉ cần gọi nó

Hộp thư lớp:
    tìm kiếm def (tự):
        ... # code để tìm kiếm hộp thư

Tài liệu lớp:
    tìm kiếm def (tự):
        ... # code để tìm kiếm tài liệu

obj.search()

ủy quyền là gì?

Ủy quyền là một kỹ thuật hướng đối tượng (còn gọi là mẫu thiết kế). Giả sử bạn có một đối tượng x và muốn thay đổi hành vi của một trong các phương thức của nó. Bạn có thể tạo một lớp mới cung cấp cách triển khai mới của phương thức mà bạn muốn thay đổi và ủy quyền tất cả các phương thức khác cho phương thức tương ứng của x.

Lập trình viên Python có thể dễ dàng thực hiện ủy quyền. Ví dụ: lớp sau triển khai một lớp hoạt động giống như một tệp nhưng chuyển đổi tất cả dữ liệu được ghi thành chữ hoa:

lớp UpperOut:

    def __init__(self, outfile):
        self._outfile = outfile

    def write(self, s):
        self._outfile.write(s.upper())

    def __getattr__(bản thân, tên):
        trả về getattr(self._outfile, name)

Ở đây, lớp UpperOut định nghĩa lại phương thức write() để chuyển đổi chuỗi đối số thành chữ hoa trước khi gọi phương thức self._outfile.write() cơ bản. Tất cả các phương thức khác được ủy quyền cho đối tượng self._outfile cơ bản. Việc ủy ​​quyền được thực hiện thông qua phương thức __getattr__(); tham khảo the language reference để biết thêm thông tin về việc kiểm soát quyền truy cập thuộc tính.

Lưu ý rằng đối với những trường hợp tổng quát hơn, việc ủy quyền có thể phức tạp hơn. Khi các thuộc tính phải được đặt cũng như được truy xuất, lớp cũng phải xác định một phương thức __setattr__() và nó phải thực hiện điều đó một cách cẩn thận. Việc triển khai cơ bản của __setattr__() gần tương đương với những điều sau:

lớp X:
    ...
    def __setattr__(bản thân, tên, giá trị):
        self.__dict__[name] = giá trị
    ...

Nhiều triển khai __setattr__() gọi object.__setattr__() để đặt thuộc tính cho chính nó mà không gây ra đệ quy vô hạn

lớp X:
    def __setattr__(bản thân, tên, giá trị):
        logic # Custom đây...
        object.__setattr__(bản thân, tên, giá trị)

Ngoài ra, có thể đặt thuộc tính bằng cách chèn trực tiếp các mục vào self.__dict__.

Làm cách nào để gọi một phương thức được xác định trong lớp cơ sở từ lớp dẫn xuất mở rộng nó?

Sử dụng chức năng super() tích hợp:

lớp phái sinh ( sở):
    def meth(tự):
        super().meth() # calls Base.meth

Trong ví dụ này, super() sẽ tự động xác định phiên bản mà nó được gọi (giá trị self), tra cứu method resolution order (MRO) với type(self).__mro__ và trả về dòng tiếp theo sau Derived trong MRO: Base.

Làm cách nào tôi có thể sắp xếp mã của mình để giúp thay đổi lớp cơ sở dễ dàng hơn?

Bạn có thể gán lớp cơ sở cho một bí danh và lấy được từ bí danh đó. Sau đó, tất cả những gì bạn phải thay đổi là giá trị được gán cho bí danh. Ngẫu nhiên, thủ thuật này cũng hữu ích nếu bạn muốn quyết định một cách linh hoạt (chẳng hạn như tùy thuộc vào nguồn tài nguyên sẵn có) nên sử dụng lớp cơ sở nào. Ví dụ:

lớp  sở:
    ...

BaseAlias =  sở

lớp phái sinh (BaseAlias):
    ...

Làm cách nào để tạo dữ liệu lớp tĩnh và phương thức lớp tĩnh?

Cả dữ liệu tĩnh và phương thức tĩnh (theo nghĩa C++ hoặc Java) đều được hỗ trợ trong Python.

Đối với dữ liệu tĩnh, chỉ cần xác định thuộc tính lớp. Để gán một giá trị mới cho thuộc tính, bạn phải sử dụng rõ ràng tên lớp trong bài tập:

lớp C:
    count = 0 # number số lần C.__init__ được gọi

    định nghĩa __init__(tự):
        C.count = C.count + 1

    def getcount(self):
        trả về C.count # or trả về self.count

c.count cũng đề cập đến C.count cho bất kỳ c nào mà isinstance(c, C) giữ, trừ khi bị ghi đè bởi chính c hoặc bởi một số lớp trên đường dẫn tìm kiếm lớp cơ sở từ c.__class__ trở lại C.

Thận trọng: trong phương thức của C, một phép gán như self.count = 42 sẽ tạo một phiên bản mới và không liên quan có tên là "count" trong lệnh riêng của self. Việc gắn lại tên dữ liệu tĩnh lớp phải luôn chỉ định lớp cho dù có bên trong phương thức hay không

C.đếm = 314

Các phương pháp tĩnh có thể thực hiện được:

lớp C:
    @staticmethod
    def tĩnh (arg1, arg2, arg3):
        thông số # No 'tự'!
        ...

Tuy nhiên, một cách đơn giản hơn nhiều để có được hiệu quả của một phương thức tĩnh là thông qua một hàm cấp mô-đun đơn giản

chắc chắn getcount():
    trả về C.count

Nếu mã của bạn được cấu trúc để xác định một lớp (hoặc phân cấp lớp có liên quan chặt chẽ) cho mỗi mô-đun, thì điều này sẽ cung cấp khả năng đóng gói mong muốn.

Làm cách nào tôi có thể quá tải các hàm tạo (hoặc phương thức) trong Python?

Câu trả lời này thực sự áp dụng cho tất cả các phương thức, nhưng câu hỏi thường xuất hiện đầu tiên trong bối cảnh của hàm tạo.

Trong C++ bạn sẽ viết:

lớp C {
    C() { cout << "Không có đối số\n"; }
    C(int i) { cout << "Đối số là " << i << "\n"; }
}

Trong Python bạn phải viết một hàm tạo duy nhất để bắt tất cả các trường hợp sử dụng các đối số mặc định. Ví dụ:

lớp C:
    def __init__(self, i=None):
        nếu tôi  Không:
            print("Không có đối số")
        khác:
            print("Đối số là", i)

Điều này không hoàn toàn tương đương, nhưng đủ gần trong thực tế.

Bạn cũng có thể thử danh sách đối số có độ dài thay đổi, ví dụ:

định nghĩa __init__(self, *args):
    ...

Cách tiếp cận tương tự áp dụng cho tất cả các định nghĩa phương thức.

Tôi cố gắng sử dụng __spam và gặp lỗi về _SomeClassName__spam.

Tên biến có dấu gạch dưới kép ở đầu được "sửa sai" để cung cấp một cách đơn giản nhưng hiệu quả để xác định các biến riêng của lớp. Bất kỳ mã định danh nào có dạng __spam (ít nhất hai dấu gạch dưới ở đầu, nhiều nhất là một dấu gạch dưới ở cuối) đều được thay thế bằng văn bản bằng _classname__spam, trong đó classname là tên lớp hiện tại với mọi dấu gạch dưới ở đầu bị loại bỏ.

Mã định danh có thể được sử dụng không thay đổi trong lớp, nhưng để truy cập nó bên ngoài lớp, phải sử dụng tên đọc sai:

lớp A:
    def __one(tự):
        trở lại 1
    def hai (tự):
        trả về 2 * self.__one()

lớp B(A):
    def ba (tự):
        trả về 3 * self._A__one()

bốn = 4 * A()._A__one()

Đặc biệt, điều này không đảm bảo quyền riêng tư vì người dùng bên ngoài vẫn có thể cố tình truy cập thuộc tính riêng tư; nhiều lập trình viên Python không bao giờ bận tâm đến việc sử dụng tên biến riêng tư.

Xem thêm

Zz000zz để biết chi tiết và các trường hợp đặc biệt.

Lớp của tôi định nghĩa __del__ nhưng nó không được gọi khi tôi xóa đối tượng.

Có một số lý do có thể cho việc này.

Câu lệnh del không nhất thiết phải gọi __del__() -- nó chỉ đơn giản là giảm số lượng tham chiếu của đối tượng và nếu số này đạt đến 0 thì __del__() sẽ được gọi.

Nếu cấu trúc dữ liệu của bạn chứa các liên kết vòng tròn (ví dụ: một cây trong đó mỗi phần tử con có một tham chiếu cha và mỗi phần tử cha có một danh sách các phần tử con) thì số lượng tham chiếu sẽ không bao giờ trở về 0. Thỉnh thoảng Python chạy một thuật toán để phát hiện các chu kỳ như vậy, nhưng trình thu gom rác có thể chạy một thời gian sau khi tham chiếu cuối cùng đến cấu trúc dữ liệu của bạn biến mất, vì vậy phương thức __del__() của bạn có thể được gọi vào thời điểm bất tiện và ngẫu nhiên. Điều này thật bất tiện nếu bạn đang cố gắng tái tạo một vấn đề. Tệ hơn nữa, thứ tự thực thi các phương thức __del__() của đối tượng là tùy ý. Bạn có thể chạy gc.collect() để buộc thu thập, nhưng có những trường hợp bệnh lý are trong đó các đối tượng sẽ không bao giờ được thu thập.

Bất chấp trình thu thập chu trình, bạn vẫn nên xác định một phương thức close() rõ ràng trên các đối tượng được gọi bất cứ khi nào bạn thực hiện xong với chúng. Phương thức close() sau đó có thể loại bỏ các thuộc tính tham chiếu đến các đối tượng con. Đừng gọi trực tiếp __del__() -- __del__() nên gọi close()close() phải đảm bảo rằng nó có thể được gọi nhiều lần cho cùng một đối tượng.

Một cách khác để tránh các tham chiếu theo chu kỳ là sử dụng mô-đun weakref, cho phép bạn trỏ đến các đối tượng mà không cần tăng số lượng tham chiếu của chúng. Ví dụ: cấu trúc dữ liệu cây nên sử dụng các tham chiếu yếu cho các tham chiếu cha mẹ và anh chị em của chúng (nếu chúng cần chúng!).

Cuối cùng, nếu phương thức __del__() của bạn đưa ra một ngoại lệ, một thông báo cảnh báo sẽ được in ra sys.stderr.

Làm cách nào để có được danh sách tất cả các phiên bản của một lớp nhất định?

Python không theo dõi tất cả các phiên bản của một lớp (hoặc loại có sẵn). Bạn có thể lập trình hàm tạo của lớp để theo dõi tất cả các phiên bản bằng cách giữ một danh sách các tham chiếu yếu cho từng phiên bản.

Tại sao kết quả của id() có vẻ không phải là duy nhất?

Nội trang id() trả về một số nguyên được đảm bảo là duy nhất trong suốt thời gian tồn tại của đối tượng. Vì trong CPython, đây là địa chỉ bộ nhớ của đối tượng nên thường xảy ra trường hợp sau khi một đối tượng bị xóa khỏi bộ nhớ, đối tượng mới được tạo tiếp theo sẽ được phân bổ ở cùng một vị trí trong bộ nhớ. Điều này được minh họa bằng ví dụ này:

>>> id(1000)
13901272
>>> id(2000)
13901272

Hai id thuộc về các đối tượng số nguyên khác nhau được tạo trước đó và bị xóa ngay sau khi thực hiện lệnh gọi id(). Để chắc chắn rằng các đối tượng có id bạn muốn kiểm tra vẫn còn tồn tại, hãy tạo một tham chiếu khác đến đối tượng:

>>> a = 1000; b = 2000
>>> id(a)
13901272
>>> id(b)
13891296

Khi nào tôi có thể dựa vào các bài kiểm tra danh tính bằng toán tử is?

Toán tử is kiểm tra nhận dạng đối tượng. Bài kiểm tra a is b tương đương với id(a) == id(b).

Thuộc tính quan trọng nhất của kiểm tra danh tính là một đối tượng luôn giống hệt chính nó, a is a luôn trả về True. Kiểm tra danh tính thường nhanh hơn kiểm tra đẳng thức. Và không giống như kiểm tra tính bằng nhau, kiểm tra danh tính được đảm bảo trả về giá trị boolean True hoặc False.

Tuy nhiên, kiểm tra danh tính only có thể được thay thế cho kiểm tra đẳng thức khi danh tính đối tượng được đảm bảo. Nói chung, có ba trường hợp danh tính được đảm bảo:

  1. Bài tập tạo tên mới nhưng không thay đổi nhận dạng đối tượng. Sau khi chuyển nhượng new = old, đảm bảo rằng new is old.

  2. Việc đặt một đối tượng vào vùng chứa lưu trữ các tham chiếu đối tượng không làm thay đổi nhận dạng đối tượng. Sau khi gán danh sách s[0] = x, đảm bảo rằng s[0] is x.

  3. Nếu một đối tượng là singleton, điều đó có nghĩa là chỉ có một phiên bản của đối tượng đó có thể tồn tại. Sau các phép gán a = Noneb = None, đảm bảo rằng a is bNone là một singleton.

Trong hầu hết các trường hợp khác, các bài kiểm tra danh tính là không nên và các bài kiểm tra bình đẳng được ưu tiên hơn. Đặc biệt, không nên sử dụng kiểm tra danh tính để kiểm tra các hằng số như intstr không được đảm bảo là các hằng số đơn lẻ:

>>> a = 10_000_000
>>> b = 5_000_000
>>> c = b + 5_000_000
>>> a  c
sai

>>> a = 'Python'
>>> b = 'Py'
>>> c = b + 'thon'
>>> a  c
sai

Tương tự như vậy, các phiên bản mới của vùng chứa có thể thay đổi không bao giờ giống nhau

>>> a = []
>>> b = []
>>> a  b
sai

Trong mã thư viện chuẩn, bạn sẽ thấy một số mẫu phổ biến để sử dụng chính xác các bài kiểm tra danh tính:

  1. Theo khuyến nghị của PEP 8, kiểm tra danh tính là cách ưu tiên để kiểm tra None. Điều này đọc giống như tiếng Anh đơn giản trong mã và tránh nhầm lẫn với các đối tượng khác có thể có giá trị boolean được đánh giá là sai.

  2. Việc phát hiện các đối số tùy chọn có thể khó khăn khi None là giá trị đầu vào hợp lệ. Trong những tình huống đó, bạn có thể tạo một đối tượng canh gác đơn lẻ được đảm bảo khác biệt với các đối tượng khác. Ví dụ: đây là cách triển khai một phương thức hoạt động giống như dict.pop():

    _sentinel = đối tượng()
    
    def pop(self, key, default=_sentinel):
        nếu tự khóa:
            giá trị = bản thân [khóa]
            del self[key]
            giá trị trả về
        nếu mặc định  _sentinel:
            tăng KeyError(key)
        trả về mặc định
    
  3. Việc triển khai vùng chứa đôi khi cần tăng cường kiểm tra tính bằng nhau bằng kiểm tra danh tính. Điều này giúp mã không bị nhầm lẫn bởi các đối tượng như float('NaN') không bằng chính chúng.

Ví dụ: đây là cách triển khai collections.abc.Sequence.__contains__():

def __contains__(bản thân, giá trị):
    cho v trong chính :
        nếu v  giá trị hoặc v == giá trị:
            trả về Đúng
    trả về Sai

Làm cách nào một lớp con có thể kiểm soát dữ liệu nào được lưu trữ trong một phiên bản bất biến?

Khi phân lớp một loại bất biến, hãy ghi đè phương thức __new__() thay vì phương thức __init__(). Cái sau chỉ chạy after một phiên bản được tạo, đã quá muộn để thay đổi dữ liệu trong một phiên bản bất biến.

Tất cả các lớp bất biến này đều có chữ ký khác với lớp cha của chúng:

nhập ngày giờ dưới dạng dt

lớp FirstOfMonthDate(dt.date):
    "Luôn chọn ngày đầu tiên của tháng"
    def __new__(cls, năm, tháng, ngày):
        trả về super().__new__(cls, năm, tháng, 1)

lớp NamedInt(int):
    "Cho phép tên văn bản cho một số số"
    xlat = {'không': 0, 'một': 1, 'mười': 10}
    def __new__(cls, value):
        giá trị = cls.xlat.get(giá trị, giá trị)
        trả về super().__new__(cls, value)

lớp TitleStr(str):
    "Chuyển đổi str thành tên phù hợp với đường dẫn URL"
    định nghĩa __new__(cls, s):
        s = s.low().replace(' ', '-')
        s = ''.join([c for c in s if c.isalnum() or c == '-'])
        trả về super().__new__(cls, s)

Các lớp có thể được sử dụng như thế này:

>>> FirstOfMonthDate(2012, 2, 14)
FirstOfMonthDate(2012, 2, 1)
>>> NamedInt('ten')
10
>>> NamedInt(20)
20
>>> TitleStr('Blog: Why Python Rocks')
'blog-why-python-rocks'

Làm cách nào để lưu vào bộ nhớ đệm các cuộc gọi phương thức?

Hai công cụ chính cho phương pháp lưu vào bộ nhớ đệm là functools.cached_property()functools.lru_cache(). Cái trước lưu trữ kết quả ở cấp độ cá thể và cái sau ở cấp độ lớp.

Cách tiếp cận cached_property chỉ hoạt động với các phương thức không có bất kỳ đối số nào. Nó không tạo ra một tham chiếu đến thể hiện. Kết quả của phương thức được lưu trong bộ nhớ cache sẽ chỉ được lưu giữ miễn là phiên bản còn tồn tại.

Ưu điểm là khi một phiên bản không còn được sử dụng nữa, kết quả của phương thức được lưu trong bộ nhớ đệm sẽ được giải phóng ngay lập tức. Điểm bất lợi là nếu các trường hợp tích lũy thì kết quả của phương pháp tích lũy cũng sẽ như vậy. Họ có thể phát triển mà không bị ràng buộc.

Cách tiếp cận lru_cache hoạt động với các phương thức có đối số hashable. Nó tạo ra một tham chiếu đến thể hiện trừ khi có nỗ lực đặc biệt để chuyển các tham chiếu yếu.

Ưu điểm của thuật toán ít được sử dụng gần đây nhất là bộ đệm được giới hạn bởi maxsize được chỉ định. Điểm bất lợi là các phiên bản vẫn được duy trì cho đến khi hết bộ nhớ đệm hoặc cho đến khi bộ nhớ đệm bị xóa.

Ví dụ này cho thấy các kỹ thuật khác nhau:

lớp học Thời tiết:
    "Tra cứu thông tin thời tiết trên website chính phủ"

    def __init__(self, station_id):
        self._station_id = trạm_id
        # The _station_id là riêng tư và không thể thay đổi

    def hiện tại_nhiệt độ (tự):
        "Quan sát hàng giờ mới nhất"
        # Do không lưu vào bộ nhớ đệm này vì kết quả cũ
        # can đã lỗi thời.

    @cached_property
    vị trí def (tự):
        "Trả về tọa độ kinh độ/vĩ độ của trạm"
        # Result chỉ phụ thuộc vào station_id

    @lru_cache(maxsize=20)
    def lịch sử_rainfall(tự, ngày, đơn vị='mm'):
        "Lượng mưa vào một ngày nhất định"
        # Depends trên station_id, ngày và đơn vị.

Ví dụ trên giả định rằng station_id không bao giờ thay đổi. Nếu các thuộc tính phiên bản liên quan có thể thay đổi thì phương pháp cached_property không thể hoạt động được vì nó không thể phát hiện các thay đổi đối với các thuộc tính.

Để làm cho phương pháp lru_cache hoạt động khi station_id có thể thay đổi, lớp cần xác định các phương thức __eq__()__hash__() để bộ đệm có thể phát hiện các cập nhật thuộc tính có liên quan:

lớp học Thời tiết:
    "Ví dụ về mã định danh trạm có thể thay đổi"

    def __init__(self, station_id):
        self.station_id = trạm_id

    def thay đổi_station(self, station_id):
        self.station_id = trạm_id

    def __eq__(bản thân, người khác):
        trả về self.station_id == other.station_id

    chắc chắn __hash__(tự):
        hàm băm trả về(self.station_id)

    @lru_cache(maxsize=20)
    def lịch sử_rainfall(tự, ngày, đơn vị='cm'):
        'Lượng mưa vào một ngày nhất định'
        # Depends trên station_id, ngày và đơn vị.

Mô-đun

Làm cách nào để tạo tệp .pyc?

Khi mô-đun được nhập lần đầu tiên (hoặc khi tệp nguồn đã thay đổi kể từ khi tệp được biên dịch hiện tại được tạo), tệp .pyc chứa mã đã biên dịch sẽ được tạo trong thư mục con __pycache__ của thư mục chứa tệp .py. Tệp .pyc sẽ có tên tệp bắt đầu bằng cùng tên với tệp .py và kết thúc bằng .pyc, với thành phần ở giữa phụ thuộc vào tệp nhị phân python cụ thể đã tạo ra nó. (Xem PEP 3147 để biết chi tiết.)

Một lý do khiến tệp .pyc có thể không được tạo là do vấn đề về quyền đối với thư mục chứa tệp nguồn, nghĩa là không thể tạo thư mục con __pycache__. Điều này có thể xảy ra, chẳng hạn như nếu bạn phát triển với tư cách một người dùng nhưng lại chạy với tư cách một người dùng khác, chẳng hạn như nếu bạn đang thử nghiệm với máy chủ web.

Trừ khi biến môi trường PYTHONDONTWRITEBYTECODE được đặt, việc tạo tệp .pyc sẽ tự động nếu bạn đang nhập mô-đun và Python có khả năng (quyền, dung lượng trống, v.v.) để tạo thư mục con __pycache__ và ghi mô-đun đã biên dịch vào thư mục con đó.

Chạy Python trên tập lệnh cấp cao nhất không được coi là nhập và sẽ không có .pyc nào được tạo. Ví dụ: nếu bạn có một mô-đun cấp cao nhất foo.py nhập một mô-đun xyz.py khác, khi bạn chạy foo (bằng cách nhập python foo.py làm lệnh shell), một .pyc sẽ được tạo cho xyzxyz được nhập nhưng sẽ không có tệp .pyc nào được tạo cho foofoo.py không được nhập.

Nếu bạn cần tạo tệp .pyc cho foo -- nghĩa là tạo tệp .pyc cho mô-đun không được nhập -- bạn có thể sử dụng mô-đun py_compilecompileall.

Mô-đun py_compile có thể biên dịch thủ công bất kỳ mô-đun nào. Một cách là sử dụng chức năng compile() trong mô-đun đó một cách tương tác

>>> import py_compile
>>> py_compile.compile('foo.py')

Thao tác này sẽ ghi .pyc vào thư mục con __pycache__ ở cùng vị trí với foo.py (hoặc bạn có thể ghi đè thư mục đó bằng tham số tùy chọn cfile).

Bạn cũng có thể tự động biên dịch tất cả các tệp trong một thư mục hoặc nhiều thư mục bằng mô-đun compileall. Bạn có thể thực hiện điều đó từ dấu nhắc shell bằng cách chạy compileall.py và cung cấp đường dẫn của thư mục chứa các tệp Python để biên dịch

python -m biên dịch.

Làm cách nào để tìm tên mô-đun hiện tại?

Một mô-đun có thể tìm ra tên mô-đun của chính nó bằng cách xem biến toàn cục được xác định trước __name__. Nếu giá trị này có giá trị '__main__' thì chương trình đang chạy dưới dạng tập lệnh. Nhiều mô-đun thường được sử dụng bằng cách nhập chúng cũng cung cấp giao diện dòng lệnh hoặc tự kiểm tra và chỉ thực thi mã này sau khi kiểm tra __name__:

chắc chắn chính():
    print('Đang chạy thử...')
    ...

nếu __name__ == '__main__':
    chính()

Làm cách nào tôi có thể có các mô-đun nhập lẫn nhau?

Giả sử bạn có các mô-đun sau:

foo.py:

từ thanh nhập bar_var
foo_var = 1

bar.py:

từ foo nhập foo_var
thanh_var = 2

Vấn đề là trình thông dịch sẽ thực hiện các bước sau:

  • nhập khẩu chính foo

  • Toàn cầu trống cho foo được tạo

  • foo được biên dịch và bắt đầu thực thi

  • foo nhập khẩu bar

  • Toàn cầu trống cho bar được tạo

  • bar được biên dịch và bắt đầu thực thi

  • bar nhập foo (không hoạt động vì đã có mô-đun có tên foo)

  • Cơ chế nhập cố gắng đọc foo_var từ toàn cục foo để đặt bar.foo_var = foo.foo_var

Bước cuối cùng không thành công, vì Python chưa hoàn thành việc diễn giải foo và từ điển ký hiệu chung cho foo vẫn trống.

Điều tương tự cũng xảy ra khi bạn sử dụng import foo và sau đó thử truy cập foo.foo_var bằng mã chung.

Có (ít nhất) ba cách giải quyết có thể cho vấn đề này.

Guido van Rossum khuyên bạn nên tránh mọi cách sử dụng from <module> import ... và đặt tất cả mã bên trong các hàm. Việc khởi tạo các biến toàn cục và biến lớp chỉ nên sử dụng các hằng số hoặc các hàm dựng sẵn. Điều này có nghĩa là mọi thứ từ mô-đun đã nhập đều được tham chiếu là <module>.<name>.

Jim Roskind đề xuất thực hiện các bước theo thứ tự sau trong mỗi mô-đun:

  • xuất khẩu (toàn cầu, hàm và các lớp không cần các lớp cơ sở được nhập)

  • câu lệnh import

  • mã hoạt động (bao gồm cả các giá trị toàn cầu được khởi tạo từ các giá trị đã nhập).

Van Rossum không thích cách tiếp cận này lắm vì hàng nhập khẩu xuất hiện ở một nơi xa lạ, nhưng nó có tác dụng.

Matthias Urlichs khuyên bạn nên cơ cấu lại mã của mình để ngay từ đầu việc nhập đệ quy không cần thiết.

Những giải pháp này không loại trừ lẫn nhau.

__import__('x.y.z') trả về <module 'x'>; làm cách nào để có được z?

Thay vào đó, hãy cân nhắc sử dụng chức năng tiện lợi import_module() từ importlib

z = importlib.import_module('x.y.z')

Khi tôi chỉnh sửa mô-đun đã nhập và nhập lại mô-đun đó, các thay đổi sẽ không hiển thị. Tại sao điều này xảy ra?

Vì lý do hiệu quả cũng như tính nhất quán, Python chỉ đọc tệp mô-đun vào lần đầu tiên mô-đun được nhập. Nếu không, trong một chương trình bao gồm nhiều mô-đun trong đó mỗi mô-đun nhập cùng một mô-đun cơ bản, mô-đun cơ bản sẽ được phân tích cú pháp và phân tích lại nhiều lần. Để buộc đọc lại mô-đun đã thay đổi, hãy thực hiện việc này:

nhập khẩu nhập khẩu
nhập tên mod
importlib.reload(tên mod)

Cảnh báo: kỹ thuật này không an toàn 100%. Đặc biệt, các mô-đun chứa các câu lệnh như:

từ modname nhập some_objects

sẽ tiếp tục làm việc với phiên bản cũ của các đối tượng đã nhập. Nếu mô-đun chứa các định nghĩa lớp, các phiên bản lớp hiện tại sẽ not được cập nhật để sử dụng định nghĩa lớp mới. Điều này có thể dẫn đến hành vi nghịch lý sau:

>>> nhập khẩu nhập khẩu
>>> nhập cls
>>> c = cls.C() # Create một phiên bản của C
>>> importlib.reload(cls)
<mô-đun 'cls' từ 'cls.py'>
>>> isinstance(c, cls.C) # isinstance là sai?!?
sai

Bản chất của vấn đề sẽ được làm rõ nếu bạn in ra "danh tính" của các đối tượng lớp:

>>> hex(id(c.__class__))
'0x7352a0'
>>> hex(id(cls.C))
'0x4198d0'