Di Python, kapan saya harus menggunakan fungsi alih-alih metode?

118

Zen of Python menyatakan bahwa seharusnya hanya ada satu cara untuk melakukan sesuatu - namun sering kali saya mengalami masalah dalam memutuskan kapan harus menggunakan suatu fungsi versus kapan harus menggunakan suatu metode.

Mari kita ambil contoh sepele- objek ChessBoard. Katakanlah kita membutuhkan cara untuk mendapatkan semua langkah resmi Raja yang tersedia di papan. Apakah kita menulis ChessBoard.get_king_moves () atau get_king_moves (papan_ catur)?

Berikut beberapa pertanyaan terkait yang saya lihat:

Jawaban yang saya dapatkan sebagian besar tidak meyakinkan:

Mengapa Python menggunakan metode untuk beberapa fungsionalitas (misalnya list.index ()) tetapi fungsi untuk yang lain (misalnya len (list))?

Alasan utamanya adalah sejarah. Fungsi digunakan untuk operasi yang generik untuk sekelompok tipe dan yang dimaksudkan untuk bekerja bahkan untuk objek yang tidak memiliki metode sama sekali (misalnya tupel). Juga mudah untuk memiliki fungsi yang dapat dengan mudah diterapkan ke kumpulan objek yang tidak berbentuk saat Anda menggunakan fitur fungsional Python (map (), apply () et al).

Faktanya, mengimplementasikan len (), max (), min () sebagai fungsi bawaan sebenarnya lebih sedikit kode daripada mengimplementasikannya sebagai metode untuk setiap jenis. Seseorang dapat berdebat tentang kasus individu tetapi itu adalah bagian dari Python, dan sudah terlambat untuk membuat perubahan mendasar seperti itu sekarang. Fungsinya harus tetap ada untuk menghindari kerusakan kode besar-besaran.

Meskipun menarik, penjelasan di atas tidak terlalu menjelaskan tentang strategi apa yang harus diadopsi.

Inilah salah satu alasannya - dengan metode khusus, pengembang akan bebas memilih nama metode yang berbeda, seperti getLength (), length (), getlength () atau apa pun. Python memberlakukan penamaan yang ketat sehingga fungsi umum len () bisa digunakan.

Sedikit lebih menarik. Pandangan saya adalah bahwa fungsinya dalam arti tertentu, versi antarmuka Pythonic.

Terakhir, dari Guido sendiri :

Berbicara tentang Kemampuan / Antarmuka membuat saya berpikir tentang beberapa nama metode khusus "nakal" kami. Dalam Referensi Bahasa, dikatakan, "Sebuah kelas dapat mengimplementasikan operasi tertentu yang dipanggil oleh sintaks khusus (seperti operasi aritmatika atau subskrip dan pemotongan) dengan mendefinisikan metode dengan nama khusus." Tetapi ada semua metode ini dengan nama khusus seperti __len__atau __unicode__yang tampaknya disediakan untuk kepentingan fungsi bawaan, daripada untuk dukungan sintaks. Agaknya dalam Python berbasis antarmuka, metode ini akan berubah menjadi metode yang dinamai secara teratur di ABC, jadi itu __len__akan menjadi

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

Padahal, memikirkannya lagi, saya tidak mengerti mengapa semua operasi sintaksis tidak hanya memanggil metode yang dinamai normal yang sesuai pada ABC tertentu. " <", misalnya, mungkin akan memanggil " object.lessthan" (atau mungkin " comparable.lessthan"). Jadi manfaat lain adalah kemampuan untuk menyapih Python dari keanehan nama yang rusak ini, yang menurut saya merupakan peningkatan HCI .

Hm. Saya tidak yakin saya setuju (bayangkan :-).

Ada dua bagian "dasar pemikiran Python" yang ingin saya jelaskan terlebih dahulu.

Pertama-tama, saya memilih len (x) daripada x.len () karena alasan HCI ( def __len__()datang jauh kemudian). Sebenarnya ada dua alasan yang saling terkait, keduanya HCI:

(a) Untuk beberapa operasi, notasi prefiks hanya terbaca lebih baik daripada postfix - operasi prefiks (dan infiks!) memiliki tradisi panjang dalam matematika yang menyukai notasi di mana visual membantu ahli matematika memikirkan suatu masalah. Bandingkan kemudahan kita menulis ulang rumus like x*(a+b)into x*a + x*bdengan kecanggungan melakukan hal yang sama menggunakan notasi OO mentah.

(b) Ketika saya membaca kode yang mengatakan len(x)saya tahu bahwa itu menanyakan panjang sesuatu. Ini memberi tahu saya dua hal: hasilnya adalah integer, dan argumennya adalah semacam wadah. Sebaliknya, ketika saya membaca x.len(), saya harus tahu bahwa itu xadalah semacam wadah yang mengimplementasikan antarmuka atau mewarisi dari kelas yang memiliki standar len(). Saksi kebingungan kita kadang-kadang memiliki ketika kelas yang tidak melaksanakan pemetaan memiliki get()atau keys() metode, atau sesuatu yang tidak file memilikiwrite() metode.

Mengatakan hal yang sama dengan cara lain, saya melihat 'len' sebagai operasi bawaan . Aku benci kehilangan itu. Saya tidak bisa mengatakan dengan pasti apakah Anda bersungguh-sungguh atau tidak, tetapi 'def len (self): ...' jelas terdengar seperti Anda ingin menurunkannya ke metode biasa. Saya sangat -1 tentang itu.

Bagian kedua dari alasan Python yang saya janjikan untuk menjelaskan adalah alasan mengapa saya memilih metode khusus untuk melihat __special__dan bukan hanya special. Saya mengantisipasi banyak operasi yang kelas mungkin ingin timpa, beberapa standar (misalnya __add__atau __getitem__), beberapa tidak begitu standar (misalnya acar __reduce__untuk waktu yang lama tidak memiliki dukungan dalam kode C sama sekali). Saya tidak ingin operasi khusus ini menggunakan nama metode biasa, karena kelas yang sudah ada sebelumnya, atau kelas yang ditulis oleh pengguna tanpa memori ensiklopedis untuk semua metode khusus, akan bertanggung jawab untuk secara tidak sengaja menentukan operasi yang tidak ingin mereka implementasikan , dengan kemungkinan konsekuensi yang menghancurkan. Ivan Krstić menjelaskan hal ini dengan lebih ringkas dalam pesannya, yang muncul setelah saya menulis semua ini.

- --Guido van Rossum (halaman muka: http://www.python.org/~guido/ )

Pemahaman saya tentang hal ini adalah bahwa dalam kasus-kasus tertentu, notasi prefiks lebih masuk akal (yaitu, Duck.quack lebih masuk akal daripada quack (Duck) dari sudut pandang linguistik.) Dan sekali lagi, fungsinya memungkinkan untuk "antarmuka".

Dalam kasus seperti itu, tebakan saya adalah mengimplementasikan get_king_moves hanya berdasarkan poin pertama Guido. Tapi itu masih menyisakan banyak pertanyaan terbuka tentang katakanlah, mengimplementasikan stack dan kelas antrian dengan metode push dan pop yang serupa- haruskah itu fungsi atau metode? (di sini saya akan menebak fungsi, karena saya benar-benar ingin memberi sinyal antarmuka push-pop)

TLDR: Dapatkah seseorang menjelaskan apa strategi untuk memutuskan kapan harus menggunakan fungsi vs metode?

Ceasar Bautista
sumber
2
Meh, aku selalu menganggap itu sangat sewenang-wenang. Pengetikan bebek memungkinkan untuk "antarmuka" implisit, tidak ada bedanya apakah Anda memiliki X.frobatau X.__frob__dan berdiri sendiri frob.
Cat Plus Plus
2
Meskipun saya sangat setuju dengan Anda, pada prinsipnya jawaban Anda bukanlah Pythonic. Ingatlah, "Saat menghadapi ambiguitas, tolak godaan untuk menebak." (Tentu saja, tenggat waktu akan mengubah ini, tetapi saya melakukan ini untuk kesenangan / perbaikan diri.)
Ceasar Bautista
Ini adalah satu hal yang saya tidak suka tentang python. Saya merasa jika Anda akan memaksa pengetikan cast seperti int ke string, maka buatlah itu menjadi metode. Menjengkelkan jika harus menyertakannya dalam paren dan memakan waktu.
Matt
1
Ini adalah alasan terpenting saya tidak suka Python: Anda tidak pernah tahu apakah Anda harus mencari fungsi atau metode ketika Anda ingin mencapai sesuatu. Dan bahkan menjadi lebih berbelit-belit saat Anda menggunakan pustaka tambahan dengan tipe data baru seperti vektor atau bingkai data.
vonjd
1
"Zen of Python menyatakan bahwa seharusnya hanya ada satu cara untuk melakukan sesuatu" kecuali tidak.
Tuan

Jawaban:

84

Aturan umum saya adalah ini - apakah operasi dilakukan pada objek atau objek?

jika itu dilakukan oleh objek, itu harus menjadi operasi anggota. Jika itu bisa berlaku untuk hal lain juga, atau dilakukan oleh sesuatu yang lain ke objek maka itu harus menjadi fungsi (atau mungkin anggota dari sesuatu yang lain).

Saat memperkenalkan pemrograman, adalah tradisional (meskipun penerapannya salah) untuk mendeskripsikan objek dalam istilah objek dunia nyata seperti mobil. Anda menyebut bebek, jadi mari kita lakukan itu.

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

Dalam konteks analogi "objek adalah benda nyata", adalah "benar" untuk menambahkan metode kelas untuk apa pun yang dapat dilakukan objek. Jadi, katakanlah saya ingin membunuh bebek, apakah saya menambahkan .kill () ke bebek? Tidak ... sejauh yang saya tahu hewan tidak bunuh diri. Karena itu jika saya ingin membunuh bebek, saya harus melakukan ini:

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."

Beranjak dari analogi ini, mengapa kita menggunakan metode dan kelas? Karena kami ingin berisi data dan mudah-mudahan menyusun kode kami sedemikian rupa sehingga dapat digunakan kembali dan diperluas di masa mendatang. Ini membawa kita pada gagasan enkapsulasi yang sangat disayangi oleh desain OO.

Prinsip enkapsulasi sebenarnya adalah apa yang akan terjadi: sebagai desainer Anda harus menyembunyikan segala sesuatu tentang implementasi dan internal kelas yang tidak sepenuhnya harus diakses oleh pengguna atau pengembang lain. Karena kita menangani instance kelas, ini direduksi menjadi "operasi apa yang penting pada instance ini ". Jika suatu operasi tidak spesifik instance, maka itu seharusnya bukan fungsi anggota.

TL; DR : apa yang dikatakan @Bryan. Jika beroperasi pada sebuah instance dan perlu mengakses data yang internal ke instance kelas, itu harus menjadi fungsi anggota.

arrdem
sumber
Jadi singkatnya, fungsi non-anggota beroperasi pada objek yang tidak dapat diubah, objek yang dapat berubah menggunakan fungsi-anggota? (Atau apakah ini generalisasi yang terlalu ketat? Ini untuk karya tertentu hanya karena jenis yang tidak berubah tidak memiliki status.)
Ceasar Bautista
1
Dari sudut pandang OOP yang ketat, saya rasa itu adil. Karena Python memiliki variabel publik dan privat (variabel dengan nama yang dimulai dengan __) dan tidak memberikan perlindungan akses terjamin tidak seperti Java, tidak ada yang absolut hanya karena kami memperdebatkan bahasa permisif. Namun, dalam bahasa yang kurang permisif seperti Java, ingatlah bahwa fungsi getFoo () ad setFoo () adalah norma, jadi keabadian tidak mutlak. Kode klien tidak diperbolehkan untuk membuat tugas kepada anggota.
arrdem
1
@Easar Itu tidak benar. Objek yang tidak berubah memiliki status; jika tidak, tidak akan ada yang membedakan bilangan bulat dari yang lain. Objek yang tidak berubah tidak mengubah statusnya. Secara umum, ini membuat masalah bagi semua negara bagian untuk menjadi publik. Dan dalam pengaturan itu, jauh lebih mudah untuk memanipulasi objek semua publik yang tidak dapat diubah dengan fungsi; tidak ada "hak istimewa" untuk menjadi sebuah metode.
Ben
1
@CeasarBautista ya, Ben ada benarnya. Ada tiga sekolah "utama" desain kode 1) tidak dirancang, 2) OOP dan 3) fungsional. dalam gaya fungsional, tidak ada status sama sekali. Ini adalah cara saya melihat sebagian besar kode python dirancang, dibutuhkan input dan menghasilkan output dengan sedikit efek samping. The titik dari OOP adalah bahwa segala sesuatu memiliki negara. Kelas adalah wadah untuk status, dan oleh karena itu fungsi "anggota" adalah kode yang bergantung pada status dan berbasis efek samping yang memuat status yang ditentukan di kelas setiap kali dipanggil. Python cenderung bersandar fungsional, karena itu preferensi kode non-anggota.
arrdem
1
makan (diri), omong kosong (diri), mati (diri). hahahaha
Wapiti
27

Gunakan kelas jika Anda ingin:

1) Pisahkan kode panggilan dari detail implementasi - manfaatkan abstraksi dan enkapsulasi .

2) Bila Anda ingin menggantikan objek lain - memanfaatkan polimorfisme .

3) Bila Anda ingin menggunakan kembali kode untuk objek serupa - memanfaatkan warisan .

Gunakan fungsi untuk panggilan yang masuk akal di berbagai tipe objek - misalnya, fungsi len dan repr bawaan berlaku untuk berbagai jenis objek.

Meski begitu, pilihan terkadang bergantung pada selera. Pikirkan tentang apa yang paling nyaman dan mudah dibaca untuk panggilan biasa. Misalnya, mana yang lebih baik (x.sin()**2 + y.cos()**2).sqrt()atau sqrt(sin(x)**2 + cos(y)**2)?

Raymond Hettinger
sumber
8

Berikut aturan praktisnya: jika kode bekerja pada satu contoh objek, gunakan metode. Lebih baik lagi: gunakan metode kecuali ada alasan kuat untuk menuliskannya sebagai fungsi.

Dalam contoh spesifik Anda, Anda ingin terlihat seperti ini:

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

Jangan terlalu memikirkannya. Selalu gunakan metode hingga Anda tiba di titik di mana Anda berkata kepada diri sendiri "tidak masuk akal menjadikan ini metode", dalam hal ini Anda dapat membuat fungsi.

Bryan Oakley
sumber
2
Bisakah Anda menjelaskan mengapa Anda menggunakan metode alih-alih fungsi? (Dan dapatkah Anda menjelaskan jika aturan itu masih masuk akal dalam kasus metode stack dan antrian / pop dan push?)
Ceasar Bautista
11
Aturan praktis itu tidak masuk akal. Pustaka standar itu sendiri dapat menjadi panduan kapan harus menggunakan kelas versus fungsi. heapq dan math adalah fungsi karena keduanya beroperasi pada objek python normal (float dan list) dan karena keduanya tidak perlu mempertahankan status kompleks seperti modul acak .
Raymond Hettinger
1
Apakah Anda mengatakan aturan itu tidak masuk akal karena perpustakaan standar melanggarnya? Saya rasa kesimpulan itu tidak masuk akal. Pertama, aturan praktis hanya itu - itu bukan aturan mutlak. Plus, sebagian besar perpustakaan standar tidak mengikuti aturan. OP sedang mencari beberapa pedoman sederhana, dan saya pikir saran saya sangat baik untuk seseorang yang baru memulai. Saya menghargai sudut pandang Anda.
Bryan Oakley
STL memiliki beberapa alasan bagus untuk melakukannya. Untuk matematika dan kawan-kawan, jelas tidak berguna untuk memiliki kelas karena kita tidak memiliki status untuk itu (dalam bahasa lain itu adalah kelas yang hanya memiliki fungsi statis). Untuk hal-hal yang beroperasi pada beberapa wadah yang berbeda seperti katakanlah len()itu juga dapat dikatakan bahwa desainnya masuk akal, meskipun menurut saya fungsi tidak akan terlalu buruk di sana - kami hanya memiliki konvensi lain yang len()selalu harus mengembalikan integer (tetapi dengan semua masalah tambahan dari backcomp, saya juga tidak akan menganjurkan itu untuk python)
Voo
-1 karena jawaban ini benar-benar sewenang-wenang: Anda pada dasarnya mencoba menunjukkan bahwa X lebih baik daripada Y dengan mengasumsikan bahwa X lebih baik daripada Y.
laki
7

Saya biasanya memikirkan objek seperti orang.

Atribut adalah nama orang, tinggi badan, ukuran sepatu, dll.

Metode dan fungsi adalah operasi yang dapat dilakukan orang tersebut.

Jika operasi dapat dilakukan oleh sembarang orang, tanpa memerlukan sesuatu yang unik untuk orang tertentu ini (dan tanpa mengubah apa pun pada orang tertentu ini), maka itu adalah fungsi dan harus ditulis seperti itu.

Jika suatu operasi menimpa orang tersebut (misalnya makan, berjalan, ...) atau memerlukan sesuatu yang unik untuk terlibat (seperti menari, menulis buku, ...), maka itu harus menjadi metode .

Tentu saja, tidak selalu mudah untuk menerjemahkan ini ke dalam objek tertentu yang sedang Anda kerjakan, tetapi menurut saya ini adalah cara yang baik untuk memikirkannya.

Thriveth
sumber
tetapi Anda dapat mengukur tinggi orang tua mana pun, jadi dengan logika itu seharusnya height(person), bukan person.height?
endolit
@endolith Tentu, tapi menurut saya tinggi lebih baik sebagai atribut karena Anda tidak perlu melakukan pekerjaan mewah untuk mengambilnya. Menulis fungsi untuk mengambil nomor sepertinya tidak perlu melewati rintangan yang tidak perlu.
Thriveth
@endolith Di sisi lain, jika orang tersebut adalah anak-anak yang tumbuh dan berubah tinggi, akan jelas membiarkan metode yang menangani itu, bukan fungsi.
Thriveth
Ini tidak benar. Bagaimana jika Anda punya are_married(person1, person2)? Kueri ini sangat umum dan karenanya harus menjadi fungsi dan bukan metode.
Pithikos
@Pithikos Tentu, tapi itu juga melibatkan lebih dari sekedar atribut atau tindakan yang dilakukan pada satu Objek (Orang), melainkan hubungan antara dua objek.
Berkembang
4

Umumnya saya menggunakan kelas untuk mengimplementasikan serangkaian kemampuan logis untuk beberapa hal , sehingga di sisa program saya, saya dapat bernalar tentang hal itu , tidak perlu khawatir tentang semua masalah kecil yang membentuk implementasinya.

Apa pun yang merupakan bagian dari abstraksi inti "apa yang dapat Anda lakukan dengan sesuatu " biasanya harus menjadi sebuah metode. Ini umumnya mencakup segala sesuatu yang dapat mengubah sesuatu , karena status data internal biasanya dianggap pribadi dan bukan bagian dari gagasan logis "apa yang dapat Anda lakukan dengan sesuatu ".

Ketika Anda datang ke operasi tingkat yang lebih tinggi, terutama jika mereka melibatkan banyak hal , saya menemukan mereka biasanya paling alami diekspresikan sebagai fungsi, jika mereka dapat dibangun dari abstraksi publik sesuatu tanpa memerlukan akses khusus ke internal (kecuali mereka ' re metode dari beberapa objek lain). Ini memiliki keuntungan besar bahwa ketika saya memutuskan untuk sepenuhnya menulis ulang internal tentang cara kerja barang saya (tanpa mengubah antarmuka), saya hanya memiliki serangkaian metode inti kecil untuk menulis ulang, dan kemudian semua fungsi eksternal ditulis dalam istilah metode tersebut hanya akan Bekerja. Saya menemukan bahwa bersikeras bahwa semua operasi yang harus dilakukan dengan kelas X adalah metode pada kelas X mengarah ke kelas yang terlalu rumit.

Itu tergantung pada kode yang saya tulis. Untuk beberapa program, saya memodelkannya sebagai kumpulan objek yang interaksinya menimbulkan perilaku program; di sini fungsionalitas yang paling penting terkait erat dengan satu objek, dan begitu juga diimplementasikan dalam metode, dengan fungsi utilitas yang tersebar. Untuk program lain hal yang paling penting adalah seperangkat fungsi yang memanipulasi data, dan kelas-kelas yang digunakan hanya untuk mengimplementasikan "tipe bebek" alami yang dimanipulasi oleh fungsi.

Ben
sumber
0

Anda mungkin berkata bahwa, "dalam menghadapi ambiguitas, tolak godaan untuk menebak".

Namun, itu bahkan bukan tebakan. Anda benar-benar yakin bahwa hasil dari kedua pendekatan itu sama dalam memecahkan masalah Anda.

Saya percaya bahwa memiliki banyak cara untuk mencapai tujuan adalah hal yang baik. Dengan rendah hati saya akan memberi tahu Anda, seperti yang telah dilakukan pengguna lain, untuk menggunakan mana saja yang "terasa lebih enak" / terasa lebih intuitif , dalam hal bahasa.

João Silva
sumber