Siapa pun yang bermain-main dengan Python cukup lama telah digigit (atau tercabik-cabik) oleh masalah berikut:
def foo(a=[]):
a.append(5)
return a
Pemula Python harapkan fungsi ini untuk selalu kembali daftar dengan hanya satu elemen: [5]
. Hasilnya malah sangat berbeda, dan sangat mencengangkan (untuk pemula):
>>> foo()
[5]
>>> foo()
[5, 5]
>>> foo()
[5, 5, 5]
>>> foo()
[5, 5, 5, 5]
>>> foo()
Seorang manajer saya pernah mengalami pertemuan pertama dengan fitur ini, dan menyebutnya "cacat desain dramatis" bahasa. Saya menjawab bahwa perilaku itu memiliki penjelasan yang mendasarinya, dan memang sangat membingungkan dan tidak terduga jika Anda tidak memahami bagian dalam. Namun, saya tidak dapat menjawab (untuk diri saya sendiri) pertanyaan berikut: apa alasan untuk mengikat argumen default pada definisi fungsi, dan bukan pada eksekusi fungsi? Saya ragu perilaku yang berpengalaman memiliki penggunaan praktis (yang benar-benar menggunakan variabel statis dalam C, tanpa membiakkan bug?)
Edit :
Baczek membuat contoh yang menarik. Bersama dengan sebagian besar komentar Anda dan Utaal khususnya, saya menjabarkan lebih lanjut:
>>> def a():
... print("a executed")
... return []
...
>>>
>>> def b(x=a()):
... x.append(5)
... print(x)
...
a executed
>>> b()
[5]
>>> b()
[5, 5]
Bagi saya, tampaknya keputusan desain itu relatif terhadap di mana menempatkan lingkup parameter: di dalam fungsi atau "bersama" dengannya?
Melakukan pengikatan di dalam fungsi akan berarti bahwa x
secara efektif terikat ke default yang ditentukan ketika fungsi dipanggil, tidak didefinisikan, sesuatu yang akan menghadirkan cacat yang dalam: def
garis akan menjadi "hibrid" dalam arti bahwa bagian dari pengikatan (dari objek fungsi) akan terjadi pada definisi, dan bagian (penugasan parameter default) pada waktu pemanggilan fungsi.
Perilaku aktual lebih konsisten: semua baris itu akan dievaluasi ketika garis dieksekusi, artinya pada definisi fungsi.
sumber
[5]
" Saya seorang pemula Python, dan saya tidak akan mengharapkan ini, karena jelasfoo([1])
akan kembali[1, 5]
, tidak[5]
. Apa yang ingin Anda katakan adalah bahwa seorang pemula akan mengharapkan fungsi yang dipanggil tanpa parameter akan selalu kembali[5]
.Jawaban:
Sebenarnya, ini bukan cacat desain, dan itu bukan karena internal, atau kinerja.
Itu datang hanya dari fakta bahwa fungsi dalam Python adalah objek kelas satu, dan tidak hanya sepotong kode.
Segera setelah Anda berpikir seperti ini, maka itu benar-benar masuk akal: suatu fungsi adalah objek yang sedang dievaluasi pada definisinya; parameter default adalah jenis "data anggota" dan karenanya statusnya dapat berubah dari satu panggilan ke panggilan lainnya - persis seperti pada objek lainnya.
Bagaimanapun, Effbot memiliki penjelasan yang sangat bagus tentang alasan perilaku ini di Default Parameter Values in Python .
Saya menemukan ini sangat jelas, dan saya benar-benar menyarankan membacanya untuk pengetahuan yang lebih baik tentang cara kerja objek fungsi.
sumber
functions are objects
. Dalam paradigma Anda, proposal akan mengimplementasikan nilai-nilai default fungsi sebagai properti daripada atribut.Misalkan Anda memiliki kode berikut
Ketika saya melihat deklarasi makan, hal yang paling mengejutkan adalah berpikir bahwa jika parameter pertama tidak diberikan, itu akan sama dengan tuple
("apples", "bananas", "loganberries")
Namun, seharusnya nanti dalam kode, saya melakukan sesuatu seperti
maka jika parameter default terikat pada eksekusi fungsi daripada deklarasi fungsi maka saya akan terkejut (dengan cara yang sangat buruk) untuk menemukan bahwa buah telah diubah. Ini akan menjadi IMO yang lebih mencengangkan daripada menemukan bahwa
foo
fungsi Anda di atas telah mengubah daftar.Masalah sebenarnya terletak pada variabel yang bisa berubah, dan semua bahasa memiliki masalah ini sampai batas tertentu. Inilah pertanyaan: misalkan di Jawa saya memiliki kode berikut:
Sekarang, apakah peta saya menggunakan nilai
StringBuffer
kunci saat ditempatkan ke peta, atau apakah menyimpan kunci dengan referensi? Bagaimanapun, seseorang heran; baik orang yang mencoba mengeluarkan objek dariMap
menggunakan nilai yang identik dengan yang mereka masukkan, atau orang yang tampaknya tidak dapat mengambil objek mereka meskipun kunci yang mereka gunakan secara harfiah adalah objek yang sama yang digunakan untuk memasukkannya ke dalam peta (inilah sebabnya mengapa Python tidak mengizinkan tipe data bawaan yang dapat diubah untuk digunakan sebagai kunci kamus).Contoh Anda adalah contoh bagus di mana pendatang baru Python akan terkejut dan digigit. Tapi saya berpendapat bahwa jika kita "memperbaiki" ini, maka itu hanya akan menciptakan situasi yang berbeda di mana mereka akan digigit sebagai gantinya, dan yang satu akan menjadi kurang intuitif. Selain itu, ini selalu terjadi ketika berhadapan dengan variabel yang bisa berubah; Anda selalu mengalami kasus di mana seseorang secara intuitif dapat mengharapkan satu atau perilaku yang berlawanan tergantung pada kode apa yang mereka tulis.
Saya pribadi menyukai pendekatan Python saat ini: argumen fungsi default dievaluasi ketika fungsi didefinisikan dan objek itu selalu menjadi default. Saya kira mereka bisa menggunakan case khusus menggunakan daftar kosong, tetapi casing khusus semacam itu akan menyebabkan lebih banyak kejutan, belum lagi mundur tidak kompatibel.
sumber
("blueberries", "mangos")
.some_random_function()
ditambahkan kefruits
alih-alih menetapkan untuk itu, perilakueat()
akan berubah. Begitu banyak untuk desain yang indah saat ini. Jika Anda menggunakan argumen default yang direferensikan di tempat lain dan kemudian memodifikasi referensi dari luar fungsi, Anda meminta masalah. WTF yang sebenarnya adalah ketika orang mendefinisikan argumen default baru (daftar literal atau panggilan ke konstruktor), dan masih mendapatkan sedikit.global
dan menetapkan kembali tuple - sama sekali tidak ada yang mengejutkan jikaeat
bekerja secara berbeda setelah itu.Bagian yang relevan dari dokumentasi :
sumber
function.data = []
) atau lebih baik lagi, membuat objek.Saya tidak tahu apa-apa tentang pekerjaan internal interpreter Python (dan saya juga bukan ahli dalam kompiler dan penerjemah) jadi jangan salahkan saya jika saya mengusulkan sesuatu yang tidak masuk akal atau tidak mungkin.
Asalkan objek python bisa berubah, saya pikir ini harus diperhitungkan ketika merancang hal-hal argumen default. Saat Anda membuat daftar:
Anda berharap mendapatkan daftar baru yang dirujuk oleh
a
.Kenapa harus
a=[]
diinstantiate daftar baru pada definisi fungsi dan bukan pada doa? Seperti halnya Anda bertanya "jika pengguna tidak memberikan argumen, instantiate daftar baru dan gunakan seolah-olah itu dibuat oleh pemanggil". Saya pikir ini ambigu sebagai gantinya:
pengguna, apakah Anda ingin
a
default ke datetime terkait ketika Anda mendefinisikan atau mengeksekusix
? Dalam hal ini, seperti pada yang sebelumnya, saya akan menjaga perilaku yang sama seperti jika argumen default "tugas" adalah instruksi pertama dari fungsi (datetime.now()
dipanggil pada pemanggilan fungsi). Di sisi lain, jika pengguna menginginkan pemetaan definisi-waktu, ia dapat menulis:Saya tahu, saya tahu: itu adalah penutupan. Atau Python mungkin menyediakan kata kunci untuk memaksa pengikatan waktu-definisi:
sumber
class {}
blok yang akan ditafsirkan sebagai milik instance :) Tapi ketika kelas adalah objek kelas satu, jelas hal yang alami adalah isi mereka (dalam memori) untuk mencerminkan konten mereka (dalam kode).Nah, alasannya cukup sederhana yaitu binding dilakukan ketika kode dieksekusi, dan definisi fungsi dieksekusi, well ... ketika fungsi didefinisikan.
Bandingkan ini:
Kode ini menderita kejadian tak terduga yang sama persis. pisang adalah atribut kelas, dan karenanya, ketika Anda menambahkan sesuatu ke dalamnya, pisang ditambahkan ke semua instance kelas itu. Alasannya persis sama.
Itu hanya "Bagaimana Cara Kerjanya", dan membuatnya bekerja secara berbeda dalam kasus fungsi mungkin akan menjadi rumit, dan dalam kasus kelas kemungkinan tidak mungkin, atau setidaknya memperlambat banyak objek objek, karena Anda harus menyimpan kode kelas di sekitar dan jalankan ketika objek dibuat.
Ya, itu tidak terduga. Tapi begitu uangnya turun, itu cocok sekali dengan cara kerja Python secara umum. Faktanya, ini adalah alat bantu pengajaran yang baik, dan sekali Anda memahami mengapa ini terjadi, Anda akan grok python jauh lebih baik.
Yang mengatakan itu harus menonjol dalam setiap tutorial Python yang baik. Karena seperti yang Anda sebutkan, semua orang mengalami masalah ini cepat atau lambat.
sumber
property
s - yang sebenarnya adalah fungsi tingkat kelas yang bertindak seperti atribut normal tetapi menyimpan atribut dalam contoh alih-alih kelas (dengan menggunakanself.attribute = value
seperti kata Lennart).Kenapa kamu tidak introspeksi?
Saya sangat terkejut tidak ada yang melakukan introspeksi mendalam yang ditawarkan oleh Python (
2
dan3
berlaku) pada callable.Diberikan fungsi kecil yang sederhana
func
didefinisikan sebagai:Ketika Python menemukannya, hal pertama yang akan dilakukan adalah mengkompilasinya untuk membuat
code
objek untuk fungsi ini. Sementara langkah kompilasi ini dilakukan, Python mengevaluasi * dan kemudian menyimpan argumen default (daftar kosong di[]
sini) di objek fungsi itu sendiri . Seperti jawaban teratas disebutkan: daftara
sekarang dapat dianggap sebagai anggota fungsifunc
.Jadi, mari kita lakukan introspeksi, sebelum dan sesudahnya untuk memeriksa bagaimana daftar tersebut diperluas di dalam objek fungsi. Saya menggunakan
Python 3.x
untuk ini, untuk Python 2 berlaku sama (gunakan__defaults__
ataufunc_defaults
Python 2; ya, dua nama untuk hal yang sama).Fungsi Sebelum Eksekusi:
Setelah Python mengeksekusi definisi ini, ia akan mengambil parameter default yang ditentukan (di
a = []
sini) dan menjejalkannya dalam__defaults__
atribut untuk objek fungsi (bagian yang relevan: Callable):Ok, jadi daftar kosong sebagai entri tunggal
__defaults__
, seperti yang diharapkan.Fungsi Setelah Eksekusi:
Sekarang mari kita jalankan fungsi ini:
Sekarang, mari kita lihat
__defaults__
lagi:Heran? Nilai di dalam objek berubah! Panggilan berurutan ke fungsi sekarang hanya akan menambahkan
list
objek tertanam :Jadi, begitulah, alasan mengapa 'cacat' ini terjadi, adalah karena argumen default adalah bagian dari objek fungsi. Tidak ada yang aneh terjadi di sini, itu semua hanya sedikit mengejutkan.
Solusi umum untuk memerangi ini adalah dengan menggunakan
None
sebagai default dan kemudian menginisialisasi dalam fungsi tubuh:Karena badan fungsi dijalankan lagi setiap kali, Anda selalu mendapatkan daftar kosong baru jika tidak ada argumen yang diteruskan
a
.Untuk lebih jauh memverifikasi bahwa daftar dalam
__defaults__
adalah sama dengan yang digunakan dalam fungsi,func
Anda hanya dapat mengubah fungsi Anda untuk mengembalikanid
daftar yanga
digunakan di dalam tubuh fungsi. Kemudian, bandingkan dengan daftar di__defaults__
(posisi[0]
di__defaults__
) dan Anda akan melihat bagaimana ini memang merujuk ke contoh daftar yang sama:Semua dengan kekuatan introspeksi!
* Untuk memverifikasi bahwa Python mengevaluasi argumen default selama kompilasi fungsi, coba jalankan yang berikut:
seperti yang akan Anda perhatikan,
input()
dipanggil sebelum proses membangun fungsi dan mengikatnya dengan namabar
dibuat.sumber
id(...)
diperlukan untuk verifikasi terakhir itu, atau apakahis
operator akan menjawab pertanyaan yang sama?is
akan baik-baik saja, saya hanya menggunakanid(val)
karena saya pikir itu mungkin lebih intuitif.None
sebagai standar sangat membatasi kegunaan dari__defaults__
introspeksi, jadi saya tidak berpikir itu berfungsi dengan baik sebagai pertahanan memiliki__defaults__
cara bekerja seperti itu. Malas-evaluasi akan berbuat lebih banyak untuk menjaga fungsi default berguna dari kedua sisi.Saya dulu berpikir bahwa membuat objek saat runtime akan menjadi pendekatan yang lebih baik. Saya kurang yakin sekarang, karena Anda kehilangan beberapa fitur yang berguna, meskipun mungkin sepadan tanpa hanya untuk mencegah kebingungan pemula. Kerugian dari melakukannya adalah:
1. Kinerja
Jika evaluasi waktu panggilan digunakan, maka fungsi yang mahal dipanggil setiap kali fungsi Anda digunakan tanpa argumen. Anda akan membayar harga mahal pada setiap panggilan, atau perlu secara manual menyimpan nilai secara eksternal, mencemari namespace Anda dan menambahkan verbosity.
2. Memaksa parameter terikat
Sebuah trik yang berguna adalah parameter mengikat dari lambda ke saat pengikatan variabel ketika lambda dibuat. Sebagai contoh:
Ini mengembalikan daftar fungsi yang mengembalikan 0,1,2,3 ... masing-masing. Jika perilaku diubah, mereka malah akan mengikat
i
ke nilai waktu panggilan dari i, sehingga Anda akan mendapatkan daftar fungsi yang semuanya dikembalikan9
.Satu-satunya cara untuk mengimplementasikan hal ini adalah dengan membuat penutupan lebih lanjut dengan i bound, yaitu:
3. Introspeksi
Pertimbangkan kodenya:
Kami dapat memperoleh informasi tentang argumen dan default menggunakan
inspect
modul, yangInformasi ini sangat berguna untuk hal-hal seperti pembuatan dokumen, metaprogramming, dekorator dll.
Sekarang, anggap perilaku default dapat diubah sehingga ini setara dengan:
Namun, kita telah kehilangan kemampuan untuk introspeksi, dan melihat apa yang argumen default adalah . Karena objek belum dibangun, kita tidak pernah bisa mendapatkannya tanpa benar-benar memanggil fungsi. Yang terbaik yang bisa kami lakukan adalah menyimpan kode sumber dan mengembalikannya sebagai string.
sumber
5 poin dalam membela Python
Kesederhanaan : Perilaku ini sederhana dalam pengertian berikut: Kebanyakan orang jatuh ke dalam perangkap ini hanya sekali, tidak beberapa kali.
Konsistensi : Python selalu melewati objek, bukan nama. Parameter default, jelas, adalah bagian dari tajuk fungsi (bukan badan fungsi). Oleh karena itu harus dievaluasi pada waktu pemuatan modul (dan hanya pada waktu pemuatan modul, kecuali bersarang), bukan pada waktu panggilan fungsi.
Kegunaan : Seperti yang ditunjukkan oleh Frederik Lundh dalam penjelasannya tentang "Nilai Parameter Default dengan Python" , perilaku saat ini dapat sangat berguna untuk pemrograman tingkat lanjut. (Gunakan dengan hemat.)
Dokumentasi yang memadai : Dalam dokumentasi Python paling mendasar, tutorialnya, masalah ini diumumkan dengan keras sebagai "Peringatan penting" di subbagian pertama Bagian "Lebih Banyak tentang Menentukan Fungsi" . Peringatan itu bahkan menggunakan huruf tebal, yang jarang diterapkan di luar judul. RTFM: Baca manual yang bagus.
Meta-learning : Jatuh ke dalam perangkap sebenarnya adalah momen yang sangat membantu (setidaknya jika Anda adalah pembelajar reflektif), karena Anda akan lebih memahami titik "Konsistensi" di atas dan itu akan mengajarkan Anda banyak tentang Python.
sumber
Perilaku ini mudah dijelaskan oleh:
Begitu:
a
tidak berubah - setiap panggilan tugas membuat objek int baru - objek baru dicetakb
tidak berubah - array baru dibuat dari nilai default dan dicetakc
perubahan - operasi dilakukan pada objek yang sama - dan itu dicetaksumber
__iadd__
, tetapi tidak bekerja dengan int. Tentu saja. :-)Yang Anda tanyakan adalah mengapa ini:
tidak secara internal setara dengan ini:
kecuali untuk kasus secara eksplisit memanggil func (Tidak Ada, Tidak Ada), yang akan kita abaikan.
Dengan kata lain, alih-alih mengevaluasi parameter default, mengapa tidak menyimpannya masing-masing, dan mengevaluasinya ketika fungsi dipanggil?
Satu jawaban mungkin ada di sana - itu akan secara efektif mengubah setiap fungsi dengan parameter default menjadi penutupan. Sekalipun semuanya disembunyikan dalam interpreter dan bukan dengan penutupan penuh, data harus disimpan di suatu tempat. Akan lebih lambat dan menggunakan lebih banyak memori.
sumber
1) Masalah yang disebut "Mutable Default Argument" secara umum adalah contoh khusus yang menunjukkan bahwa:
"Semua fungsi dengan masalah ini juga mengalami masalah efek samping yang serupa pada parameter aktual ,"
Itu bertentangan dengan aturan pemrograman fungsional, biasanya tidak layak dan harus diperbaiki bersama-sama.
Contoh:
Solusi : salinan Solusi yang
benar-benar aman adalah untuk
copy
ataudeepcopy
objek input pertama dan kemudian melakukan apa pun dengan salinan.Banyak tipe yang bisa berubah-ubah memiliki metode salin seperti
some_dict.copy()
atausome_set.copy()
atau dapat disalin dengan mudah sepertisomelist[:]
ataulist(some_list)
. Setiap objek dapat juga disalin olehcopy.copy(any_object)
atau lebih teliticopy.deepcopy()
(yang terakhir berguna jika objek yang dapat diubah terdiri dari objek yang dapat diubah). Beberapa objek pada dasarnya didasarkan pada efek samping seperti "file" objek dan tidak dapat direproduksi secara bermakna dengan menyalin. penyalinanContoh masalah untuk pertanyaan SO yang serupa
Seharusnya tidak disimpan dalam atribut publik dari instance yang dikembalikan oleh fungsi ini. (Dengan asumsi bahwa atribut pribadi dari instance tidak boleh dimodifikasi dari luar kelas ini atau subkelas dengan konvensi. Yaitu
_var1
atribut pribadi)Kesimpulan:
Input parameter objek tidak boleh dimodifikasi di tempat (bermutasi) atau mereka tidak boleh diikat menjadi objek yang dikembalikan oleh fungsi. (Jika kita lebih suka pemrograman tanpa efek samping yang sangat dianjurkan. Lihat Wiki tentang "efek samping" (Dua paragraf pertama adalah relevan dalam konteks ini.).)
2)
Hanya jika efek samping pada parameter aktual diperlukan tetapi tidak diinginkan pada parameter default maka solusi yang berguna
def ...(var1=None):
if var1 is None:
var1 = []
Lebih ..3) Dalam beberapa kasus perilaku mutable dari parameter default berguna .
sumber
def f( a = None )
konstruk ini direkomendasikan ketika Anda benar-benar bermaksud sesuatu yang lain. Menyalin tidak masalah, karena Anda tidak boleh bermutasi argumen. Dan ketika Anda melakukannyaif a is None: a = [1, 2, 3]
, Anda tetap menyalin daftar itu.Ini sebenarnya tidak ada hubungannya dengan nilai default, selain itu sering muncul sebagai perilaku yang tidak terduga ketika Anda menulis fungsi dengan nilai default yang bisa berubah.
Tidak ada nilai default yang terlihat dalam kode ini, tetapi Anda mendapatkan masalah yang sama persis.
Masalahnya adalah bahwa
foo
adalah memodifikasi variabel bisa berubah berlalu dalam dari pemanggil, ketika penelepon tidak mengharapkan ini. Kode seperti ini akan baik-baik saja jika fungsinya disebut sesuatu sepertiappend_5
; maka pemanggil akan memanggil fungsi untuk mengubah nilai yang mereka berikan, dan perilaku akan diharapkan. Tetapi fungsi seperti itu akan sangat tidak mungkin untuk mengambil argumen default, dan mungkin tidak akan mengembalikan daftar (karena penelepon sudah memiliki referensi ke daftar itu; yang baru saja diteruskan).Dokumen asli Anda
foo
, dengan argumen default, tidak boleh memodifikasia
apakah itu secara eksplisit diteruskan atau mendapat nilai default. Kode Anda harus meninggalkan argumen yang dapat diubah sendiri kecuali jelas dari konteks / nama / dokumentasi bahwa argumen tersebut seharusnya dimodifikasi. Menggunakan nilai-nilai yang bisa diubah yang dilewatkan sebagai argumen sebagai temporal lokal adalah ide yang sangat buruk, apakah kita menggunakan Python atau tidak dan apakah ada argumen default yang terlibat atau tidak.Jika Anda perlu memanipulasi temporer lokal secara destruktif saat menghitung sesuatu, dan Anda perlu memulai manipulasi dari nilai argumen, Anda perlu membuat salinan.
sumber
append
akan berubaha
"di tempat"). Bahwa mutable default tidak instantiated pada setiap panggilan adalah bit "tak terduga" ... setidaknya bagi saya. :)cache={}
. Namun, saya menduga ini "paling mencengangkan" muncul adalah ketika Anda tidak mengharapkan (atau menginginkan) fungsi yang Anda panggil untuk mengubah argumen.cache={}
ke dalamnya untuk kelengkapan.None
dan menetapkan default nyata jika argNone
tidak menyelesaikan masalah itu (saya menganggapnya sebagai pola anti karena alasan itu). Jika Anda memperbaiki bug lain dengan menghindari mutasi nilai argumen apakah mereka memiliki default atau tidak, maka Anda tidak akan pernah memperhatikan atau peduli dengan perilaku "menakjubkan" ini.Sudah topik sibuk, tetapi dari apa yang saya baca di sini, berikut ini membantu saya menyadari bagaimana itu bekerja secara internal:
sumber
a = a + [1]
kelebihana
... pertimbangkan untuk mengubahnyab = a + [1] ; print id(b)
dan menambahkan barisa.append(2)
. Itu akan membuatnya lebih jelas bahwa+
pada dua daftar selalu membuat daftar baru (ditugaskan keb
), sementara yang diubaha
masih dapat memiliki yang samaid(a)
.Ini adalah optimasi kinerja. Sebagai hasil dari fungsionalitas ini, manakah dari kedua panggilan fungsi yang menurut Anda lebih cepat?
Saya akan memberi Anda petunjuk. Inilah pembongkarannya (lihat http://docs.python.org/library/dis.html ):
#
1#
2Seperti yang Anda lihat, ada adalah manfaat kinerja saat menggunakan argumen default berubah. Ini dapat membuat perbedaan jika itu adalah fungsi yang sering disebut atau argumen default membutuhkan waktu lama untuk dibangun. Juga, ingatlah bahwa Python bukan C. Di C Anda memiliki konstanta yang cukup bebas. Dengan Python Anda tidak memiliki manfaat ini.
sumber
Python: Argumen Default yang Dapat Berubah
Argumen default dievaluasi pada saat fungsi dikompilasi menjadi objek fungsi. Ketika digunakan oleh fungsi, beberapa kali oleh fungsi itu, mereka adalah dan tetap menjadi objek yang sama.
Ketika mereka bisa berubah, ketika bermutasi (misalnya, dengan menambahkan elemen ke dalamnya) mereka tetap bermutasi pada panggilan berturut-turut.
Mereka tetap bermutasi karena mereka adalah objek yang sama setiap kali.
Kode Setara:
Karena daftar terikat ke fungsi ketika objek fungsi dikompilasi dan dipakai, ini:
hampir persis sama dengan ini:
Demonstrasi
Ini sebuah demonstrasi - Anda dapat memverifikasi bahwa mereka adalah objek yang sama setiap kali direferensikan oleh
example.py
dan menjalankannya dengan
python example.py
:Apakah ini melanggar prinsip "Least Astonishment"?
Urutan eksekusi ini seringkali membingungkan bagi pengguna baru Python. Jika Anda memahami model eksekusi Python, maka itu menjadi sangat diharapkan.
Instruksi biasa untuk pengguna Python baru:
Tapi ini sebabnya instruksi biasa kepada pengguna baru adalah membuat argumen default mereka seperti ini:
Ini menggunakan singleton None sebagai objek penjaga untuk memberi tahu fungsi apakah kita mendapatkan argumen selain default. Jika kami tidak mendapatkan argumen, maka kami sebenarnya ingin menggunakan daftar kosong baru
[]
,, sebagai default.Seperti yang dikatakan bagian tutorial tentang aliran kontrol :
sumber
Jawaban terpendek mungkin adalah "definisi adalah eksekusi", oleh karena itu seluruh argumen tidak masuk akal. Sebagai contoh yang lebih dibuat-buat, Anda dapat mengutip ini:
Mudah-mudahan itu cukup untuk menunjukkan bahwa tidak mengeksekusi ekspresi argumen default pada waktu eksekusi
def
pernyataan itu tidak mudah atau tidak masuk akal, atau keduanya.Saya setuju itu gotcha ketika Anda mencoba menggunakan konstruktor default.
sumber
Solusi sederhana menggunakan Tidak Ada
sumber
Perilaku ini tidak mengejutkan jika Anda mempertimbangkan hal berikut:
Peran (2) telah dibahas secara luas di utas ini. (1) kemungkinan merupakan faktor penyebab keheranan, karena perilaku ini tidak "intuitif" ketika datang dari bahasa lain.
(1) dijelaskan dalam tutorial Python di kelas . Dalam upaya untuk menetapkan nilai ke atribut kelas read-only:
Lihat kembali contoh aslinya dan pertimbangkan poin-poin di atas:
Berikut
foo
adalah objek dana
atributfoo
(tersedia difoo.func_defs[0]
). Karenaa
adalah daftar,a
bisa berubah dan dengan demikian merupakan atribut baca-tulis darifoo
. Ini diinisialisasi ke daftar kosong seperti yang ditentukan oleh tanda tangan ketika fungsi ini dipakai, dan tersedia untuk membaca dan menulis selama objek fungsi ada.Memanggil
foo
tanpa mengesampingkan default menggunakan nilai default darifoo.func_defs
. Dalam hal ini,foo.func_defs[0]
digunakan untuka
dalam lingkup kode objek fungsi. Perubahan untuka
berubahfoo.func_defs[0]
, yang merupakan bagian darifoo
objek dan berlanjut antara eksekusi kode difoo
.Sekarang, bandingkan ini dengan contoh dari dokumentasi tentang meniru perilaku argumen default bahasa lain , sedemikian rupa sehingga default tanda tangan fungsi digunakan setiap kali fungsi dijalankan:
Mempertimbangkan (1) dan (2) , seseorang dapat melihat mengapa ini mencapai perilaku yang diinginkan:
foo
objek fungsi instantiated,foo.func_defs[0]
diatur keNone
, objek yang tidak dapat diubah.L
panggilan fungsi),foo.func_defs[0]
(None
) tersedia dalam lingkup lokal sebagaiL
.L = []
, tugas tidak dapat berhasilfoo.func_defs[0]
, karena atribut itu hanya baca.L
dibuat dalam lingkup lokal dan digunakan untuk sisa panggilan fungsi.foo.func_defs[0]
dengan demikian tetap tidak berubah untuk doa di masa depanfoo
.sumber
Saya akan mendemonstrasikan struktur alternatif untuk meneruskan nilai daftar default ke suatu fungsi (berfungsi sama baiknya dengan kamus).
Seperti yang orang lain telah berkomentar secara luas, parameter daftar terikat ke fungsi ketika didefinisikan sebagai berlawanan dengan ketika dieksekusi. Karena daftar dan kamus dapat diubah, setiap perubahan pada parameter ini akan memengaruhi panggilan lain ke fungsi ini. Akibatnya, panggilan berikutnya ke fungsi akan menerima daftar bersama ini yang mungkin telah diubah oleh panggilan lain ke fungsi. Lebih buruk lagi, dua parameter menggunakan parameter yang dibagikan fungsi ini pada saat yang sama tidak menyadari perubahan yang dilakukan oleh yang lain.
Metode yang salah (mungkin ...) :
Anda dapat memverifikasi bahwa mereka adalah satu dan objek yang sama dengan menggunakan
id
:Per Brett Slatkin "Python Efektif: 59 Cara Khusus untuk Menulis Python Lebih Baik", Butir 20: Gunakan
None
dan Docstrings untuk menentukan argumen default dinamis (p. 48)Implementasi ini memastikan bahwa setiap panggilan ke fungsi menerima daftar default atau daftar yang diteruskan ke fungsi.
Metode yang disukai :
Mungkin ada kasus penggunaan yang sah untuk 'Metode Salah' di mana pemrogram bermaksud parameter daftar default untuk dibagikan, tetapi ini lebih merupakan pengecualian daripada aturan.
sumber
Solusi di sini adalah:
None
sebagai nilai default Anda (atau nonceobject
), dan aktifkan itu untuk membuat nilai Anda saat runtime; ataulambda
sebagai parameter default Anda, dan panggil dalam blok coba untuk mendapatkan nilai default (ini adalah jenis hal yang diperuntukkan bagi abstraksi lambda).Opsi kedua bagus karena pengguna fungsi dapat meneruskan panggilan, yang mungkin sudah ada (seperti a
type
)sumber
Ketika kita melakukan ini:
... kami memberikan argumen
a
kepada yang tidak disebutkan namanya daftar , jika penelepon tidak memberikan nilai a.Untuk mempermudah diskusi ini, mari kita sementara waktu memberikan nama yang tidak disebutkan namanya. Bagaimana dengan
pavlo
?Kapan saja, jika penelepon tidak memberi tahu kami apa yang
a
ada, kami menggunakan kembalipavlo
.Jika
pavlo
dapat diubah (dapat dimodifikasi), danfoo
akhirnya memodifikasinya, efek yang kami perhatikan waktu berikutnyafoo
dipanggil tanpa menentukana
.Jadi ini yang Anda lihat (Ingat,
pavlo
diinisialisasi ke []):Sekarang,
pavlo
adalah [5].Memanggil
foo()
lagi memodifikasipavlo
lagi:Menentukan
a
kapan panggilanfoo()
memastikanpavlo
tidak tersentuh.Jadi,
pavlo
tetap saja[5, 5]
.sumber
Saya terkadang mengeksploitasi perilaku ini sebagai alternatif dari pola berikut:
Jika
singleton
hanya digunakan olehuse_singleton
, saya suka pola berikut sebagai pengganti:Saya telah menggunakan ini untuk membuat instance kelas klien yang mengakses sumber daya eksternal, dan juga untuk membuat dikte atau daftar untuk memoisasi.
Karena saya pikir pola ini tidak dikenal, saya memberikan komentar singkat untuk menjaga agar tidak terjadi kesalahpahaman di masa depan.
sumber
_make_singleton
pada waktu def dalam contoh argumen default, tetapi pada waktu panggilan dalam contoh global. Substitusi yang benar akan menggunakan semacam kotak yang bisa berubah-ubah untuk nilai argumen default, tetapi penambahan argumen membuat peluang untuk memberikan nilai alternatif.Anda bisa menyelesaikan ini dengan mengganti objek (dan karenanya mengikat dengan cakupan):
Jelek, tapi berhasil.
sumber
Mungkin benar bahwa:
sepenuhnya konsisten untuk berpegang pada kedua fitur di atas dan masih membuat poin lain:
Jawaban lainnya, atau setidaknya beberapa dari mereka membuat poin 1 dan 2 tetapi tidak 3, atau membuat poin 3 dan mengecilkan poin 1 dan 2. Tetapi ketiganya benar.
Mungkin benar bahwa menukar kuda di tengah sungai di sini akan meminta kerusakan yang signifikan, dan bahwa mungkin ada lebih banyak masalah yang diciptakan dengan mengubah Python untuk secara intuitif menangani cuplikan pembuka Stefano. Dan mungkin benar bahwa seseorang yang mengenal internal Python dengan baik dapat menjelaskan ladang ranjau konsekuensi.Namun,
Perilaku yang ada bukan Pythonic, dan Python berhasil karena sangat sedikit tentang bahasa yang melanggar prinsip paling tidak heran dimanapun dekatini sangat buruk. Ini adalah masalah nyata, apakah bijaksana untuk mencabutnya atau tidak. Ini adalah cacat desain. Jika Anda memahami bahasa dengan lebih baik dengan mencoba melacak perilaku, saya dapat mengatakan bahwa C ++ melakukan semua ini dan lebih banyak lagi; Anda belajar banyak dengan menavigasi, misalnya, kesalahan pointer halus. Tapi ini bukan Pythonic: orang yang cukup peduli tentang Python untuk bertahan dalam menghadapi perilaku ini adalah orang-orang yang tertarik pada bahasa tersebut karena Python memiliki kejutan yang jauh lebih sedikit daripada bahasa lain. Penyeka dan penasaran menjadi Pythonista ketika mereka heran betapa sedikit waktu yang diperlukan untuk membuat sesuatu bekerja - bukan karena desain fl - maksud saya, teka-teki logika tersembunyi - yang memotong intuisi programmer yang tertarik pada Python karena itu hanya berfungsi .
sumber
x=[]
berarti "buat objek daftar kosong, dan ikat nama 'x' ke sana." Jadi, dalamdef f(x=[])
, daftar kosong juga dibuat. Tidak selalu terikat ke x, jadi alih-alih terikat ke pengganti default. Kemudian ketika f () dipanggil, defaultnya diangkut keluar dan terikat ke x. Karena itu adalah daftar kosong itu sendiri yang disingkirkan, daftar yang sama adalah satu-satunya yang tersedia untuk mengikat ke x, apakah ada sesuatu yang terjebak di dalamnya atau tidak. Bagaimana bisa sebaliknya?Ini bukan cacat desain . Siapa pun yang tersandung ini melakukan kesalahan.
Ada 3 kasus yang saya lihat di mana Anda mungkin mengalami masalah ini:
cache={}
, dan Anda tidak akan diharapkan untuk memanggil fungsi dengan argumen yang sebenarnya sama sekali.Contoh dalam pertanyaan dapat jatuh ke dalam kategori 1 atau 3. Aneh bahwa keduanya memodifikasi daftar yang berlalu dan mengembalikannya; Anda harus memilih satu atau yang lain.
sumber
cache={}
Pola benar-benar sebuah solusi wawancara-satunya, dalam kode nyata Anda mungkin ingin@lru_cache
!"Bug" ini memberi saya banyak jam kerja lembur! Tapi saya mulai melihat potensi penggunaannya (tapi saya ingin itu pada saat eksekusi, masih)
Saya akan memberi Anda apa yang saya lihat sebagai contoh yang bermanfaat.
mencetak yang berikut ini
sumber
Ubah saja fungsi menjadi:
sumber
Saya pikir jawaban untuk pertanyaan ini terletak pada bagaimana python meneruskan data ke parameter (lulus dengan nilai atau referensi), bukan mutabilitas atau bagaimana python menangani pernyataan "def".
Perkenalan singkat. Pertama, ada dua tipe tipe data dalam python, satu tipe data sederhana, seperti angka, dan tipe data lainnya adalah objek. Kedua, ketika meneruskan data ke parameter, python meneruskan tipe data dasar dengan nilai, yaitu, membuat salinan nilai lokal ke variabel lokal, tetapi meneruskan objek dengan referensi, yaitu, pointer ke objek.
Mengakui dua poin di atas, mari kita jelaskan apa yang terjadi pada kode python. Itu hanya karena lewat referensi untuk objek, tetapi tidak ada hubungannya dengan bisa berubah / berubah, atau bisa dibilang fakta bahwa pernyataan "def" dieksekusi hanya sekali ketika didefinisikan.
[] adalah objek, jadi python meneruskan referensi [] ke
a
, yaitu,a
hanya sebuah pointer ke [] yang terletak di memori sebagai objek. Hanya ada satu salinan [] dengan, bagaimanapun, banyak referensi untuk itu. Untuk foo pertama (), daftar [] diubah menjadi 1 dengan metode append. Tetapi Perhatikan bahwa hanya ada satu salinan dari objek daftar dan objek ini sekarang menjadi 1 . Saat menjalankan foo kedua (), apa yang dikatakan halaman web effbot (item tidak dievaluasi lagi) salah.a
dievaluasi menjadi objek daftar, meskipun sekarang konten objek adalah 1 . Ini adalah efek lewat referensi! Hasil foo (3) dapat dengan mudah diturunkan dengan cara yang sama.Untuk lebih memvalidasi jawaban saya, mari kita lihat dua kode tambahan.
====== No. 2 ========
[]
adalah sebuah objek, demikian jugaNone
(yang pertama bisa berubah sementara yang terakhir tidak dapat diubah. Tapi ketidakmampuan tidak ada hubungannya dengan pertanyaan). Tidak ada yang berada di suatu tempat di ruang itu tetapi kita tahu itu ada di sana dan hanya ada satu salinan Tidak ada di sana. Jadi setiap kali foo dipanggil, item dievaluasi (sebagai lawan dari beberapa jawaban yang hanya dievaluasi sekali) menjadi Tidak, untuk menjadi jelas, referensi (atau alamat) dari Tidak ada. Kemudian di foo, item diubah ke [], yaitu, menunjuk ke objek lain yang memiliki alamat berbeda.====== No. 3 =======
Doa foo (1) membuat item menunjuk ke objek daftar [] dengan alamat, misalnya, 11111111. konten daftar diubah menjadi 1 pada fungsi foo dalam sekuel, tetapi alamatnya tidak berubah, masih 11111111 Kemudian foo (2, []) akan datang. Meskipun [] di foo (2, []) memiliki konten yang sama dengan parameter default [] saat memanggil foo (1), alamatnya berbeda! Karena kami memberikan parameter secara eksplisit,
items
harus mengambil alamat yang baru ini[]
, katakan 2222222, dan mengembalikannya setelah melakukan beberapa perubahan. Sekarang foo (3) dieksekusi. sejak itu sajax
disediakan, item harus mengambil nilai standarnya lagi. Apa nilai standarnya? Sudah diatur ketika mendefinisikan fungsi foo: objek daftar terletak di 11111111. Jadi item dievaluasi menjadi alamat 11111111 memiliki elemen 1. Daftar yang terletak di 2222222 juga berisi satu elemen 2, tetapi tidak ditunjukkan oleh item apa pun lebih. Akibatnya, sebuah tambahan 3 akan menghasilkanitems
[1,3].Dari penjelasan di atas, kita dapat melihat bahwa halaman web effbot yang direkomendasikan dalam jawaban yang diterima gagal memberikan jawaban yang relevan untuk pertanyaan ini. Terlebih lagi, saya pikir titik di halaman web effbot salah. Saya pikir kode mengenai UI. Tombol sudah benar:
Setiap tombol dapat memiliki fungsi panggilan balik yang berbeda yang akan menampilkan nilai yang berbeda
i
. Saya dapat memberikan contoh untuk menunjukkan ini:Jika kita mengeksekusi
x[7]()
kita akan mendapatkan 7 seperti yang diharapkan, danx[9]()
akan memberi 9, nilai lain darii
.sumber
x[7]()
adalah9
.TLDR: Tentukan waktu default konsisten dan lebih ekspresif.
Mendefinisikan suatu fungsi memengaruhi dua cakupan: lingkup pendefinisian yang berisi fungsi, dan cakupan eksekusi yang terkandung oleh fungsi. Meskipun cukup jelas bagaimana memblokir peta ke ruang lingkup, pertanyaannya adalah di mana
def <name>(<args=defaults>):
milik:The
def name
bagian harus mengevaluasi dalam ruang lingkup mendefinisikan - kami inginname
menjadi tersedia di sana, setelah semua. Mengevaluasi fungsi hanya di dalam dirinya sendiri akan membuatnya tidak dapat diakses.Karena
parameter
ini adalah nama yang konstan, kita dapat "mengevaluasinya" bersamaan dengandef name
. Ini juga memiliki kelebihan yang menghasilkan fungsi dengan tanda tangan yang dikenal sebagainame(parameter=...):
, bukannya telanjangname(...):
.Sekarang, kapan harus mengevaluasi
default
?Konsistensi sudah mengatakan "pada definisi": segala sesuatu
def <name>(<args=defaults>):
yang terbaik dievaluasi pada definisi juga. Menunda bagian dari itu akan menjadi pilihan yang menakjubkan.Kedua pilihan tidak setara, baik: Jika
default
dievaluasi pada waktu definisi, itu masih dapat mempengaruhi waktu eksekusi. Jikadefault
dievaluasi pada waktu pelaksanaan, itu tidak dapat mempengaruhi waktu definisi. Memilih "pada definisi" memungkinkan mengekspresikan kedua kasus, sementara memilih "saat eksekusi" hanya dapat mengungkapkan satu:sumber
def <name>(<args=defaults>):
yang terbaik dievaluasi pada definisi juga." Saya tidak berpikir kesimpulannya berasal dari premis. Hanya karena dua hal berada di jalur yang sama tidak berarti mereka harus dievaluasi dalam cakupan yang sama.default
adalah hal yang berbeda dari yang lainnya: itu ekspresi. Mengevaluasi suatu ekspresi adalah proses yang sangat berbeda dari mendefinisikan suatu fungsi.def
) atau ekspresi (lambda
) tidak berubah bahwa membuat fungsi berarti evaluasi - terutama dari tanda tangannya. Dan default adalah bagian dari tanda tangan suatu fungsi. Itu tidak berarti standar harus segera dievaluasi - ketik petunjuk mungkin tidak, misalnya. Tapi tentu saja menyarankan mereka harus kecuali ada alasan bagus untuk tidak melakukannya.Setiap jawaban lain menjelaskan mengapa ini sebenarnya adalah perilaku yang baik dan diinginkan, atau mengapa Anda tidak perlu melakukan hal ini. Milik saya adalah untuk mereka yang keras kepala yang ingin menggunakan hak mereka untuk membengkokkan bahasa sesuai keinginan mereka, bukan sebaliknya.
Kami akan "memperbaiki" perilaku ini dengan dekorator yang akan menyalin nilai default alih-alih menggunakan kembali contoh yang sama untuk setiap argumen posisi yang tersisa pada nilai defaultnya.
Sekarang mari kita mendefinisikan kembali fungsi kita menggunakan dekorator ini:
Ini sangat rapi untuk fungsi yang mengambil banyak argumen. Membandingkan:
dengan
Penting untuk dicatat bahwa solusi di atas rusak jika Anda mencoba menggunakan kata kunci args, seperti:
Penghias dapat disesuaikan untuk memungkinkan untuk itu, tetapi kami meninggalkan ini sebagai latihan untuk pembaca;)
sumber