Trong Python, khi nào tôi nên sử dụng một hàm thay vì một phương thức?


118

Zen của Python nói rằng chỉ nên có một cách để thực hiện mọi việc - nhưng tôi thường xuyên gặp phải vấn đề quyết định khi nào sử dụng một hàm so với khi nào sử dụng một phương thức.

Hãy lấy một ví dụ tầm thường - một đối tượng ChessBoard. Giả sử chúng ta cần một số cách để có được tất cả các nước đi Vua hợp pháp trên bàn cờ. Chúng ta viết ChessBoard.get_king_moves () hay get_king_moves (flags_board)?

Dưới đây là một số câu hỏi liên quan mà tôi đã xem xét:

Các câu trả lời tôi nhận được phần lớn không thể kết luận được:

Tại sao Python sử dụng các phương thức cho một số chức năng (ví dụ: list.index ()) nhưng các hàm cho các chức năng khác (ví dụ: len (danh sách))?

Lý do chính là lịch sử. Các hàm được sử dụng cho những hoạt động chung cho một nhóm kiểu và được dự định để hoạt động ngay cả đối với các đối tượng hoàn toàn không có phương thức (ví dụ: bộ giá trị). Cũng rất tiện lợi khi có một hàm có thể dễ dàng được áp dụng cho một tập hợp các đối tượng vô định hình khi bạn sử dụng các tính năng chức năng của Python (map (), apply () et al).

Trên thực tế, việc triển khai len (), max (), min () dưới dạng một hàm dựng sẵn thực sự là ít mã hơn việc triển khai chúng dưới dạng các phương thức cho từng kiểu. Người ta có thể phân minh về các trường hợp riêng lẻ nhưng đó là một phần của Python và đã quá muộn để thực hiện những thay đổi cơ bản như vậy ngay bây giờ. Các chức năng phải được duy trì để tránh bị hỏng mã lớn.

Tuy thú vị nhưng những điều trên không thực sự nói lên nhiều điều về việc áp dụng chiến lược nào.

Đây là một trong những lý do - với các phương thức tùy chỉnh, các nhà phát triển có thể tự do chọn một tên phương thức khác, như getLength (), length (), getlength () hoặc bất kỳ tên nào khác. Python thực thi đặt tên nghiêm ngặt để có thể sử dụng hàm chung len ().

Thú vị hơn một chút. Ý kiến ​​của tôi là các chức năng theo một nghĩa nào đó, là phiên bản Pythonic của các giao diện.

Cuối cùng, từ chính Guido :

Nói về Khả năng / Giao diện khiến tôi nghĩ về một số tên phương pháp đặc biệt "bất hảo" của chúng tôi. Trong Tham chiếu ngôn ngữ, nó nói, "Một lớp có thể triển khai các hoạt động nhất định được gọi bằng cú pháp đặc biệt (chẳng hạn như các phép toán số học hoặc chỉ số con và cắt) bằng cách xác định các phương thức có tên đặc biệt." Nhưng có tất cả các phương thức này với các tên đặc biệt như __len__hoặc __unicode__dường như được cung cấp vì lợi ích của các hàm tích hợp, chứ không phải để hỗ trợ cú pháp. Có lẽ trong Python dựa trên giao diện, các phương thức này sẽ chuyển thành các phương thức được đặt tên thường xuyên trên ABC, vì vậy điều đó __len__sẽ trở thành

class container:
  ...
  def len(self):
    raise NotImplemented

Mặc dù vậy, suy nghĩ về nó một chút nữa, tôi không hiểu tại sao tất cả các hoạt động cú pháp sẽ không chỉ gọi phương thức được đặt tên thông thường thích hợp trên một ABC cụ thể. " <", chẳng hạn, có lẽ sẽ gọi " object.lessthan" (hoặc có thể là " comparable.lessthan"). Vì vậy, một lợi ích khác sẽ là khả năng cai nghiện Python khỏi cái tên kỳ quặc này, đối với tôi dường như đây là một cải tiến HCI .

Hừm. Tôi không chắc mình đồng ý (hình như :-).

Có hai phần về "cơ sở lý luận của Python" mà tôi muốn giải thích trước.

Trước hết, tôi chọn len (x) thay vì x.len () vì lý do HCI ( def __len__()đến sau này nhiều). Thực ra, có hai lý do đan xen lẫn nhau, cả HCI:

(a) Đối với một số phép toán, ký hiệu tiền tố chỉ đọc tốt hơn so với hậu tố - các phép toán tiền tố (và tiền tố!) có một truyền thống lâu đời trong toán học, thích các ký hiệu nơi hình ảnh giúp nhà toán học suy nghĩ về một vấn đề. Hãy so sánh dễ dàng mà chúng tôi viết lại một công thức như x*(a+b)vào x*a + x*bđể sự vụng về của làm điều tương tự bằng cách sử dụng ký hiệu OO thô.

(b) Khi tôi đọc mã nói rằng len(x)tôi biết rằng nó đang yêu cầu độ dài của một thứ gì đó. Điều này cho tôi biết hai điều: kết quả là một số nguyên và đối số là một loại vùng chứa nào đó. Ngược lại, khi tôi đọc x.len(), tôi phải biết rằng đó xlà một loại vùng chứa nào đó triển khai giao diện hoặc kế thừa từ một lớp có tiêu chuẩn len(). Chứng kiến ​​sự nhầm lẫn mà chúng tôi thỉnh thoảng gặp phải khi một lớp không triển khai ánh xạ có một get()hoặc keys() phương thức, hoặc một cái gì đó không phải là một tệp có một write()phương thức.

Nói điều tương tự theo cách khác, tôi thấy 'len' là một hoạt động được tích hợp sẵn . Tôi không muốn mất điều đó. Tôi không thể chắc chắn liệu bạn có muốn nói vậy hay không, nhưng 'def len (self): ...' chắc chắn có vẻ như bạn muốn hạ cấp nó xuống một phương thức bình thường. Tôi mạnh mẽ -1 về điều đó.

Cơ sở lý luận thứ hai của Python mà tôi hứa sẽ giải thích là lý do tại sao tôi chọn các phương pháp đặc biệt để xem xét __special__chứ không đơn thuần special. Tôi đã dự đoán rất nhiều hoạt động mà các lớp có thể muốn ghi đè, một số tiêu chuẩn (ví dụ __add__hoặc __getitem__), một số không quá tiêu chuẩn (ví dụ: pickle's __reduce__trong một thời gian dài không hỗ trợ mã C). Tôi không muốn các thao tác đặc biệt này sử dụng các tên phương thức thông thường, vì khi đó các lớp có sẵn hoặc các lớp được viết bởi người dùng không có bộ nhớ bách khoa cho tất cả các phương thức đặc biệt, sẽ có trách nhiệm vô tình xác định các thao tác mà họ không có ý thực hiện , với những hậu quả tai hại có thể xảy ra. Ivan Krstić giải thích điều này ngắn gọn hơn trong thông điệp của anh ấy, gửi đến sau khi tôi viết tất cả những điều này.

- --Guido van Rossum (trang chủ: http://www.python.org/~guido/ )

Sự hiểu biết của tôi về điều này là trong một số trường hợp nhất định, ký hiệu tiền tố chỉ có ý nghĩa hơn (tức là Duck.quack có ý nghĩa hơn quack (Vịt) theo quan điểm ngôn ngữ.) Và một lần nữa, các hàm cho phép "giao diện".

Trong trường hợp như vậy, tôi đoán sẽ triển khai get_king_moves chỉ dựa trên điểm đầu tiên của Guido. Nhưng điều đó vẫn để lại rất nhiều câu hỏi mở liên quan đến việc thực hiện một lớp ngăn xếp và hàng đợi với các phương thức push và pop tương tự - chúng nên là hàm hay phương thức? (ở đây tôi sẽ đoán các chức năng, bởi vì tôi thực sự muốn báo hiệu một giao diện push-pop)

TLDR: Ai đó có thể giải thích chiến lược để quyết định khi nào sử dụng các hàm so với các phương pháp không?


2
Meh, tôi luôn nghĩ điều đó hoàn toàn độc đoán. Kiểu gõ Duck cho phép tạo ra các "giao diện" ngầm, nó không tạo ra nhiều sự khác biệt cho dù bạn có X.frobhay X.__frob__không frob.
Cat Plus Plus

2
Trong khi tôi hầu như đồng ý với bạn, về nguyên tắc câu trả lời của bạn không phải là Pythonic. Nhớ lại, "Đối mặt với sự mơ hồ, hãy từ chối sự cám dỗ để đoán." (Tất nhiên, thời hạn sẽ thay đổi điều này, nhưng tôi đang làm điều này cho vui / tự hoàn thiện mình.)
Caesar Bautista

Đây là một điều tôi không thích về python. Tôi cảm thấy nếu bạn định ép kiểu nhập kiểu như int vào một chuỗi, thì chỉ cần đặt nó thành một phương thức. Thật khó chịu khi phải đóng gói nó trong những bức tranh nhỏ và tốn thời gian.
Matt

1
Đây là lý do quan trọng nhất mà tôi không thích Python: bạn không bao giờ biết liệu bạn phải tìm kiếm một hàm hay một phương thức khi bạn muốn đạt được điều gì đó. Và nó thậm chí còn phức tạp hơn khi bạn sử dụng các thư viện bổ sung với các kiểu dữ liệu mới như vectơ hoặc khung dữ liệu.
vonjd

1
"Zen của Python nói rằng chỉ nên có một cách để làm mọi thứ" ngoại trừ nó không.
gented

Câu trả lời:


84

Quy tắc chung của tôi là đây - hoạt động được thực hiện trên đối tượng hay bởi đối tượng?

nếu nó được thực hiện bởi đối tượng, nó phải là một hoạt động thành viên. Nếu nó cũng có thể áp dụng cho những thứ khác, hoặc được thực hiện bởi thứ khác đối với đối tượng thì nó phải là một hàm (hoặc có thể là một thành viên của thứ khác).

Khi giới thiệu lập trình, việc mô tả các đối tượng dưới dạng các đối tượng trong thế giới thực chẳng hạn như ô tô là truyền thống (mặc dù triển khai không chính xác). Bạn đề cập đến một con vịt, vì vậy chúng ta hãy đi với điều đó.

class duck: 
    def __init__(self):pass
    def eat(self, o): pass 
    def crap(self) : pass
    def die(self)
    ....

Trong ngữ cảnh của phép loại suy "đối tượng là những thứ có thật", việc thêm một phương thức lớp cho bất cứ thứ gì mà đối tượng có thể làm là "đúng". Vì vậy, giả sử tôi muốn giết một con vịt, tôi có thêm .kill () vào con vịt không? Không ... theo như tôi biết thì động vật không tự sát. Vì vậy, nếu tôi muốn giết một con vịt, tôi nên làm như sau:

def kill(o):
    if isinstance(o, duck):
        o.die()
    elif isinstance(o, dog):
        print "WHY????"
        o.die()
    elif isinstance(o, nyancat):
        raise Exception("NYAN "*9001)
    else:
       print "can't kill it."

Di chuyển khỏi phép loại suy này, tại sao chúng ta sử dụng các phương thức và lớp? Bởi vì chúng tôi muốn chứa dữ liệu và hy vọng cấu trúc mã của chúng tôi theo cách để nó sẽ có thể tái sử dụng và mở rộng trong tương lai. Điều này đưa chúng ta đến khái niệm đóng gói vốn rất quen thuộc với thiết kế OO.

Nguyên tắc đóng gói thực sự là những gì điều này xảy ra: là một nhà thiết kế, bạn nên ẩn mọi thứ về việc triển khai và nội bộ lớp mà nó không hoàn toàn nhất thiết phải cho bất kỳ người dùng hoặc nhà phát triển nào khác truy cập. Bởi vì chúng tôi xử lý các trường hợp của các lớp, điều này giảm xuống "hoạt động nào là quan trọng trên trường hợp này ". Nếu một hoạt động không phải là phiên bản cụ thể, thì nó không phải là một hàm thành viên.

TL; DR : @Bryan đã nói gì. Nếu nó hoạt động trên một cá thể và cần truy cập dữ liệu bên trong cá thể lớp, nó phải là một hàm thành viên.


Vậy tóm lại, các hàm không phải là thành viên hoạt động trên các đối tượng bất biến, các đối tượng có thể thay đổi sử dụng các hàm thành viên? (Hoặc là này một sự tổng quát quá khắt khe này cho các công trình nhất định chỉ vì loại không thay đổi không có nhà nước?.)
Caesar Bautista

1
Từ quan điểm OOP nghiêm ngặt, tôi đoán điều đó là công bằng. Vì Python có cả biến công khai và riêng tư (các biến có tên bắt đầu bằng __) và cung cấp khả năng bảo vệ truy cập không được đảm bảo, không giống như Java, không có điều kiện tuyệt đối nào đơn giản chỉ vì chúng ta đang tranh luận về một ngôn ngữ dễ dãi. Tuy nhiên, trong một ngôn ngữ ít dễ dãi hơn như Java, hãy nhớ rằng hàm getFoo () ad setFoo () là chuẩn mực, vì vậy tính bất biến không phải là tuyệt đối. Mã khách hàng chỉ không được phép gán cho các thành viên.
arrdem

1
@Ceasar Điều đó không đúng. Các đối tượng bất biến có trạng thái; nếu không sẽ không có gì để phân biệt bất kỳ số nguyên nào với bất kỳ số nguyên nào khác. Các đối tượng bất biến không thay đổi trạng thái của chúng. Nói chung, điều này làm cho tất cả trạng thái của họ được công khai ít có vấn đề hơn nhiều. Và trong cài đặt đó, việc thao tác có ý nghĩa một đối tượng công khai bất biến với các hàm dễ dàng hơn nhiều; không có "đặc quyền" là một phương thức.
Ben,

1
@CeasarBautista vâng, Ben có lý. Có ba trường phái thiết kế mã "chính" 1) không được thiết kế, 2) OOP và 3) chức năng. trong một phong cách chức năng, không có trạng thái nào cả. Đây là cách mà tôi thấy hầu hết các mã python được thiết kế, nó nhận đầu vào và tạo ra đầu ra với ít tác dụng phụ. Các điểm của OOP là mọi thứ đều có một trạng thái. Các lớp là một vùng chứa cho các trạng thái và các hàm "thành viên" do đó là mã dựa trên hiệu ứng phụ và phụ thuộc vào trạng thái để tải trạng thái được xác định trong lớp bất cứ khi nào được gọi. Python có xu hướng nghiêng về chức năng, do đó ưu tiên của mã không phải là thành viên.
arrdem

1
ăn (tự), tào (tự), chết (tự). hahahaha
Wapiti

27

Sử dụng một lớp học khi bạn muốn:

1) Cô lập mã gọi khỏi các chi tiết triển khai - tận dụng tính trừu tượngđóng gói .

2) Khi bạn muốn thay thế cho các đối tượng khác - tận dụng tính đa hình .

3) Khi bạn muốn sử dụng lại mã cho các đối tượng tương tự - tận dụng tính kế thừa .

Sử dụng một hàm cho các lệnh gọi có ý nghĩa trên nhiều loại đối tượng khác nhau - ví dụ: hàm lenrepr nội trang áp dụng cho nhiều loại đối tượng.

Tuy nhiên, sự lựa chọn đôi khi phụ thuộc vào sở thích. Hãy suy nghĩ về những gì thuận tiện và dễ đọc nhất cho các cuộc gọi thông thường. Ví dụ, cái nào sẽ tốt hơn (x.sin()**2 + y.cos()**2).sqrt()hoặc sqrt(sin(x)**2 + cos(y)**2)?


8

Đây là một quy tắc ngón tay cái đơn giản: nếu mã hoạt động trên một phiên bản duy nhất của một đối tượng, hãy sử dụng một phương thức. Tốt hơn nữa: hãy sử dụng một phương thức trừ khi có lý do thuyết phục để viết nó dưới dạng một hàm.

Trong ví dụ cụ thể của bạn, bạn muốn nó trông như thế này:

chessboard = Chessboard()
...
chessboard.get_king_moves()

Đừng nghĩ quá nhiều. Luôn luôn sử dụng các phương thức cho đến khi bạn tự nói với mình "không có ý nghĩa gì khi biến phương pháp này thành một phương pháp", trong trường hợp đó bạn có thể tạo một hàm.


2
Bạn có thể giải thích lý do tại sao bạn mặc định cho các phương thức trên các hàm không? (Và bạn có thể giải thích nếu quy tắc mà vẫn có ý nghĩa trong trường hợp của ngăn xếp và hàng đợi / pop và đẩy phương pháp?)
Caesar Bautista

11
Quy tắc ngón tay cái đó không có ý nghĩa. Bản thân thư viện chuẩn có thể là một hướng dẫn về thời điểm sử dụng các lớp so với các hàm. heapqmath là các hàm vì chúng hoạt động trên các đối tượng python bình thường (phao và danh sách) và vì chúng không cần duy trì trạng thái phức tạp như mô-đun ngẫu nhiên .
Raymond Hettinger

1
Bạn đang nói quy tắc không có ý nghĩa vì thư viện tiêu chuẩn vi phạm nó? Tôi không nghĩ rằng kết luận đó có ý nghĩa. Đối với một, các quy tắc ngón tay cái chỉ có vậy - chúng không phải là quy tắc tuyệt đối. Thêm vào đó, một phần lớn của thư viện tiêu chuẩn thực hiện theo đó quy tắc của ngón tay cái. OP đã tìm kiếm một số hướng dẫn đơn giản, và tôi nghĩ lời khuyên của tôi là hoàn toàn tốt cho những người mới bắt đầu. Tôi đánh giá cao quan điểm của bạn, tuy nhiên.
Bryan Oakley

STL có một số lý do chính đáng để làm như vậy. Đối với toán học và co rõ ràng là vô ích khi có một lớp vì chúng ta không có bất kỳ trạng thái nào cho nó (trong các ngôn ngữ khác, đó là các lớp chỉ có các hàm tĩnh). Đối với những thứ hoạt động trên một số vùng chứa khác nhau như đã nói len(), cũng có thể lập luận rằng thiết kế có ý nghĩa, mặc dù cá nhân tôi nghĩ rằng các chức năng cũng sẽ không quá tệ ở đó - chúng tôi chỉ có một quy ước khác len()luôn phải trả về số nguyên (nhưng với tất cả các vấn đề bổ sung của backcomp, tôi cũng sẽ không ủng hộ điều đó cho python)
Voo

-1 vì câu trả lời này là hoàn toàn tùy ý: về cơ bản bạn đang cố gắng chứng minh rằng X tốt hơn Y bằng cách giả định rằng X tốt hơn Y.
gented

7

Tôi thường nghĩ về một đối tượng như một người.

Các thuộc tính là tên, chiều cao, cỡ giày của người đó, v.v.

Các phương phápchức năng là các hoạt động mà người đó có thể thực hiện.

Nếu thao tác có thể được thực hiện bởi bất kỳ người nào, mà không yêu cầu bất kỳ điều gì duy nhất đối với một người cụ thể này (và không thay đổi bất kỳ điều gì trên một người cụ thể này), thì đó là một hàm và nên được viết như vậy.

Nếu một hoạt động đang tác động lên người đó (ví dụ như ăn, đi bộ, ...) hoặc yêu cầu một thứ gì đó độc đáo để người này tham gia (như khiêu vũ, viết sách, ...) thì đó phải là một phương pháp .

Tất nhiên, không phải lúc nào cũng tầm thường để dịch điều này sang một đối tượng cụ thể mà bạn đang làm việc, nhưng tôi thấy đó là một cách hay.


nhưng bạn có thể đo chiều cao của bất kỳ người già nào, vì vậy theo logic đó, nó phải là như vậy height(person), phải không person.height?
endolith

@endolith Chắc chắn rồi, nhưng tôi muốn nói rằng chiều cao tốt hơn là một thuộc tính vì bạn không cần phải thực hiện bất kỳ công việc cầu kỳ nào để lấy nó. Viết một hàm để truy xuất một số có vẻ như không cần thiết phải nhảy qua.
Thriveth

@endolith Mặt khác, nếu một người là một đứa trẻ lớn lên và thay đổi chiều cao, điều hiển nhiên là để một phương pháp chăm sóc điều đó, chứ không phải một chức năng.
Thriveth

Điều này không đúng. Nếu bạn có are_married(person1, person2)thì sao? Truy vấn này rất chung chung và do đó phải là một hàm chứ không phải một phương thức.
Pithikos

@Pithikos Chắc chắn rồi, nhưng điều đó không chỉ liên quan đến một thuộc tính hoặc hành động được thực hiện trên một Đối tượng (Người), mà còn là mối quan hệ giữa hai đối tượng.
Thriveth

4

Nói chung, tôi sử dụng các lớp để triển khai một tập hợp khả năng hợp lý cho một số thứ , để trong phần còn lại của chương trình, tôi có thể suy luận về điều đó , không phải lo lắng về tất cả các mối quan tâm nhỏ tạo nên việc triển khai nó.

Bất cứ điều gì là một phần của sự trừu tượng cốt lõi của "những gì bạn có thể làm với một thứ " thường phải là một phương pháp. Điều này thường bao gồm mọi thứ có thể thay đổi một sự vật , vì trạng thái dữ liệu bên trong thường được coi là riêng tư và không phải là một phần của ý tưởng hợp lý về "những gì bạn có thể làm với một thứ ".

Khi bạn đến với các hoạt động cấp cao hơn, đặc biệt nếu chúng liên quan đến nhiều thứ , tôi thấy chúng thường được thể hiện một cách tự nhiên nhất dưới dạng các hàm, nếu chúng có thể được xây dựng từ sự trừu tượng công khai của một thứ mà không cần truy cập đặc biệt vào nội bộ (trừ khi chúng ' lại phương thức của một số đối tượng khác). Này có lợi thế lớn là khi tôi quyết định viết lại hoàn toàn bên trong của cách tôi điều công trình (mà không thay đổi giao diện), tôi chỉ có một bộ lõi nhỏ của phương pháp để viết lại, và sau đó tất cả các chức năng bên ngoài bằng văn bản về các phương pháp đó sẽ Chỉ hoạt động. Tôi thấy rằng việc khăng khăng rằng tất cả các thao tác phải làm với lớp X là các phương thức trên lớp X dẫn đến các lớp quá phức tạp.

Nó phụ thuộc vào mã tôi đang viết. Đối với một số chương trình, tôi mô hình hóa chúng như một tập hợp các đối tượng mà các tương tác của chúng làm phát sinh hành vi của chương trình; ở đây, hầu hết chức năng quan trọng được kết hợp chặt chẽ với một đối tượng duy nhất, và do đó được triển khai trong các phương thức, với sự phân tán các chức năng tiện ích. Đối với các chương trình khác, điều quan trọng nhất là một tập hợp các hàm thao tác dữ liệu và các lớp chỉ được sử dụng để triển khai các "kiểu vịt" tự nhiên được thao tác bởi các hàm.


0

Bạn có thể nói rằng, "khi đối mặt với sự mơ hồ, hãy từ chối sự cám dỗ để đoán".

Tuy nhiên, nó thậm chí không phải là một phỏng đoán. Bạn hoàn toàn chắc chắn rằng kết quả của cả hai cách tiếp cận đều giống nhau ở chỗ chúng giải quyết được vấn đề của bạn.

Tôi tin rằng có nhiều cách để đạt được mục tiêu chỉ là một điều tốt. Tôi khiêm tốn nói với bạn, như những người dùng khác đã làm, sử dụng bất kỳ cái nào "ngon hơn" / cảm thấy trực quan hơn , về mặt ngôn ngữ.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.