"Setidaknya Terkejut" dan Argumen Default Mutable

2595

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 xsecara efektif terikat ke default yang ditentukan ketika fungsi dipanggil, tidak didefinisikan, sesuatu yang akan menghadirkan cacat yang dalam: defgaris 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.

Stefano Borini
sumber
4
Saya tidak meragukan argumen yang bisa berubah melanggar prinsip keheranan paling tidak untuk rata-rata orang, dan saya telah melihat pemula melangkah di sana, kemudian secara heroik mengganti mailing list dengan mailing tuple. Namun demikian argumen yang bisa berubah masih sejalan dengan klon Python Zen (Pep 20) dan jatuh ke klausa "jelas untuk Belanda" (dipahami / dieksploitasi oleh programmer python hard core). Pemecahan masalah yang direkomendasikan dengan string dokumen adalah yang terbaik, namun penolakan terhadap string dokumen dan dokumen (tertulis) apa pun tidak jarang terjadi saat ini. Secara pribadi, saya lebih suka dekorator (misalnya @fixed_defaults).
Serge
5
Argumen saya ketika saya menemukan ini adalah: "Mengapa Anda perlu membuat fungsi yang mengembalikan bisa berubah yang opsional bisa berubah menjadi Anda akan beralih ke fungsi? Entah itu mengubah bisa berubah atau membuat yang baru. Mengapa Anda perlu untuk melakukan keduanya dengan satu fungsi? Dan mengapa penerjemah harus ditulis ulang untuk memungkinkan Anda melakukan itu tanpa menambahkan tiga baris ke kode Anda? " Karena kita sedang berbicara tentang menulis ulang cara interpreter menangani definisi dan evokasi fungsi di sini. Itu banyak yang harus dilakukan untuk kasus penggunaan yang hampir tidak perlu.
Alan Leuthard
12
"Pemula Python mengharapkan fungsi ini untuk selalu mengembalikan daftar dengan hanya satu elemen:. [5]" Saya seorang pemula Python, dan saya tidak akan mengharapkan ini, karena jelas foo([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].
symplectomorphic
2
Pertanyaan ini bertanya, "Mengapa ini [cara yang salah] diimplementasikan?" Tidak bertanya "Apa jalan yang benar?" , yang dicakup oleh [ Mengapa menggunakan arg = Tidak ada yang memperbaiki masalah argumen default Python yang bisa berubah? ] * ( stackoverflow.com/questions/10676729/… ). Pengguna baru hampir selalu kurang tertarik pada yang pertama dan lebih banyak pada yang terakhir, jadi itu kadang-kadang tautan / dupe yang sangat berguna untuk dikutip.
smci

Jawaban:

1613

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.

rampok
sumber
80
Bagi siapa pun yang membaca jawaban di atas, saya sangat menyarankan Anda meluangkan waktu untuk membaca artikel Effbot yang tertaut. Seperti halnya semua info bermanfaat lainnya, bagian tentang bagaimana fitur bahasa ini dapat digunakan untuk cache / memoisasi hasil sangat mudah diketahui!
Cam Jackson
85
Bahkan jika itu adalah objek kelas satu, orang mungkin masih membayangkan desain di mana kode untuk setiap nilai default disimpan bersama dengan objek dan dievaluasi ulang setiap kali fungsi dipanggil. Saya tidak mengatakan itu akan lebih baik, hanya saja fungsinya menjadi objek kelas satu tidak sepenuhnya menghalanginya.
gerrit
312
Maaf, tetapi apa pun yang dianggap "WTF terbesar dengan Python" jelas merupakan cacat desain . Ini adalah sumber bug untuk semua orang di beberapa titik, karena tidak ada yang mengharapkan perilaku itu pada awalnya - yang berarti seharusnya tidak dirancang seperti itu untuk memulai. Saya tidak peduli lingkaran apa yang harus mereka lewati, mereka seharusnya mendesain Python sehingga argumen default tidak statis.
BlueRaja - Danny Pflughoeft
192
Entah itu cacat desain atau tidak, jawaban Anda tampaknya menyiratkan bahwa perilaku ini entah bagaimana perlu, alami dan jelas mengingat bahwa fungsi adalah objek kelas satu, dan itu bukan masalahnya. Python memiliki penutupan. Jika Anda mengganti argumen default dengan penugasan pada baris pertama fungsi, itu mengevaluasi ekspresi setiap panggilan (berpotensi menggunakan nama yang dideklarasikan dalam lingkup terlampir). Tidak ada alasan sama sekali bahwa tidak mungkin atau masuk akal untuk memiliki argumen default dievaluasi setiap kali fungsi dipanggil dengan cara yang persis sama.
Mark Amery
24
Desainnya tidak langsung mengikuti functions are objects. Dalam paradigma Anda, proposal akan mengimplementasikan nilai-nilai default fungsi sebagai properti daripada atribut.
bukzor
273

Misalkan Anda memiliki kode berikut

fruits = ("apples", "bananas", "loganberries")

def eat(food=fruits):
    ...

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

def some_random_function():
    global fruits
    fruits = ("blueberries", "mangos")

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 foofungsi 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:

StringBuffer s = new StringBuffer("Hello World!");
Map<StringBuffer,Integer> counts = new HashMap<StringBuffer,Integer>();
counts.put(s, 5);
s.append("!!!!");
System.out.println( counts.get(s) );  // does this work?

Sekarang, apakah peta saya menggunakan nilai StringBufferkunci saat ditempatkan ke peta, atau apakah menyimpan kunci dengan referensi? Bagaimanapun, seseorang heran; baik orang yang mencoba mengeluarkan objek dari Mapmenggunakan 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.

Eli Courtwright
sumber
30
Saya pikir ini masalah perdebatan. Anda bertindak pada variabel global. Setiap evaluasi yang dilakukan di mana saja dalam kode Anda yang melibatkan variabel global Anda sekarang akan (dengan benar) merujuk ke ("blueberry", "mangga"). parameter default bisa saja seperti kasus lainnya.
Stefano Borini
47
Sebenarnya, saya pikir saya tidak setuju dengan contoh pertama Anda. Saya tidak yakin saya suka ide memodifikasi penginisialisasi seperti itu di tempat pertama, tetapi jika saya lakukan, saya berharap itu akan berperilaku persis seperti yang Anda jelaskan - mengubah nilai default menjadi ("blueberries", "mangos").
Ben Blank
12
Parameter standar adalah seperti kasus lainnya. Apa yang tidak terduga adalah bahwa parameternya adalah variabel global, dan bukan variabel lokal. Yang pada gilirannya adalah karena kode dieksekusi pada definisi fungsi, bukan panggilan. Setelah Anda mendapatkannya, dan hal yang sama berlaku untuk kelas, itu sangat jelas.
Lennart Regebro
17
Saya menemukan contoh yang menyesatkan daripada brilian. Jika some_random_function()ditambahkan ke fruitsalih-alih menetapkan untuk itu, perilaku eat() 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.
alexis
13
Anda hanya secara eksplisit menyatakan globaldan menetapkan kembali tuple - sama sekali tidak ada yang mengejutkan jika eatbekerja secara berbeda setelah itu.
user3467349
241

Bagian yang relevan dari dokumentasi :

Nilai parameter default dievaluasi dari kiri ke kanan ketika definisi fungsi dijalankan. Ini berarti bahwa ekspresi dievaluasi satu kali, ketika fungsi didefinisikan, dan bahwa nilai "pra-komputasi" yang sama digunakan untuk setiap panggilan. Ini sangat penting untuk dipahami ketika parameter default adalah objek yang bisa berubah-ubah, seperti daftar atau kamus: jika fungsi memodifikasi objek (misalnya dengan menambahkan item ke daftar), nilai default diubah efeknya. Ini biasanya bukan yang dimaksudkan. Cara mengatasinya adalah dengan menggunakan Nonesebagai default, dan secara eksplisit mengujinya di tubuh fungsi, misalnya:

def whats_on_the_telly(penguin=None):
    if penguin is None:
        penguin = []
    penguin.append("property of the zoo")
    return penguin
glglgl
sumber
180
Ungkapan "ini umumnya bukan apa yang dimaksudkan" dan "jalan keluarnya" adalah bau seperti mereka mendokumentasikan cacat desain.
bukzor
4
@ Matthew: Saya sangat sadar, tapi itu tidak layak jebakan. Anda biasanya akan melihat panduan gaya dan linters tanpa syarat menandai nilai default yang bisa berubah sebagai salah karena alasan ini. Cara eksplisit untuk melakukan hal yang sama adalah dengan memasukkan atribut ke fungsi ( function.data = []) atau lebih baik lagi, membuat objek.
bukzor
6
@bukzor: Perangkap perlu dicatat dan didokumentasikan, itulah sebabnya pertanyaan ini bagus dan telah menerima begitu banyak upvotes. Pada saat yang sama, jebakan tidak perlu dihilangkan. Berapa banyak pemula Python yang meneruskan daftar ke fungsi yang memodifikasinya, dan terkejut melihat perubahan muncul dalam variabel asli? Namun jenis objek yang bisa berubah sangat bagus, ketika Anda mengerti cara menggunakannya. Saya kira itu hanya bermuara pada pendapat tentang perangkap khusus ini.
Matius
33
Ungkapan "ini umumnya bukan apa yang dimaksudkan" berarti "bukan apa yang sebenarnya diinginkan oleh programmer," bukan "bukan apa yang seharusnya dilakukan oleh Python."
holdenweb
4
@ holdenweb Wow, saya terlambat datang ke pesta. Mengingat konteksnya, bukzor sepenuhnya benar: mereka mendokumentasikan perilaku / konsekuensi yang tidak "dimaksudkan" ketika mereka memutuskan bahasanya harus mengeksekusi definisi fungsi. Karena ini merupakan konsekuensi yang tidak disengaja dari pilihan desain mereka, itu adalah cacat desain. Jika itu bukan cacat desain, tidak perlu bahkan menawarkan "jalan keluar".
code_dredd
118

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:

a = []

Anda berharap mendapatkan daftar baru yang dirujuk oleh a.

Kenapa harus a=[]di

def x(a=[]):

instantiate 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:

def x(a=datetime.datetime.now()):

pengguna, apakah Anda ingin adefault ke datetime terkait ketika Anda mendefinisikan atau mengeksekusi x? 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:

b = datetime.datetime.now()
def x(a=b):

Saya tahu, saya tahu: itu adalah penutupan. Atau Python mungkin menyediakan kata kunci untuk memaksa pengikatan waktu-definisi:

def x(static a=b):
Utaal
sumber
11
Anda dapat melakukan: def x (a = Tidak Ada): Dan kemudian, jika a adalah Tidak ada, tetapkan a = datetime.datetime.now ()
Anon
20
Terima kasih untuk ini. Aku tidak bisa benar-benar menekankan mengapa ini membuatku jengkel. Anda telah melakukannya dengan indah dengan sedikit kebingungan dan kebingungan. Sebagai seseorang yang datang dari pemrograman sistem di C ++ dan kadang-kadang secara naif "menerjemahkan" fitur bahasa, teman palsu ini menendang saya ke dalam soft time, seperti halnya atribut kelas. Saya mengerti mengapa hal-hal seperti ini, tetapi saya tidak bisa tidak membencinya, tidak peduli apa pun positifnya. Setidaknya itu sangat bertentangan dengan pengalaman saya, bahwa saya mungkin (semoga) tidak akan pernah melupakannya ...
AndreasT
5
@Andreas setelah Anda menggunakan Python cukup lama, Anda mulai melihat betapa logisnya bagi Python untuk menginterpretasikan hal-hal sebagai atribut kelas seperti yang dilakukannya - itu hanya karena kebiasaan khusus dan keterbatasan bahasa seperti C ++ (dan Java, dan C # ...) bahwa masuk akal untuk isi dari 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).
Karl Knechtel
6
Struktur normatif tidak ada kekhasan atau batasan dalam buku saya. Saya tahu ini bisa canggung dan jelek, tetapi Anda bisa menyebutnya sebagai "definisi" dari sesuatu. Bahasa-bahasa yang dinamis nampak seperti anarkis bagi saya: Tentu semua orang bebas, tetapi Anda perlu struktur untuk membuat seseorang mengosongkan sampah dan membuka jalan. Kira saya sudah tua ... :)
AndreasT
4
Definisi fungsi dijalankan pada waktu pemuatan modul. Fungsi tubuh dieksekusi pada saat pemanggilan fungsi. Argumen default adalah bagian dari definisi fungsi, bukan dari tubuh fungsi. (Semakin rumit untuk fungsi bersarang.)
Lutz Prechelt
84

Nah, alasannya cukup sederhana yaitu binding dilakukan ketika kode dieksekusi, dan definisi fungsi dieksekusi, well ... ketika fungsi didefinisikan.

Bandingkan ini:

class BananaBunch:
    bananas = []

    def addBanana(self, banana):
        self.bananas.append(banana)

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.

Lennart Regebro
sumber
Bagaimana Anda mendefinisikan atribut kelas yang berbeda untuk setiap instance kelas?
Kieveli
19
Jika berbeda untuk setiap instance, itu bukan atribut kelas. Atribut kelas adalah atribut pada CLASS. Maka nama. Karenanya mereka sama untuk semua contoh.
Lennart Regebro
1
Bagaimana Anda mendefinisikan atribut dalam kelas yang berbeda untuk setiap instance kelas? (Didefinisikan ulang untuk mereka yang tidak dapat menentukan bahwa seseorang yang tidak akrab dengan konvensi penamaan Python mungkin bertanya tentang variabel anggota normal suatu kelas).
Kieveli
@Kievieli: Anda berbicara tentang variabel anggota normal suatu kelas. :-) Anda mendefinisikan atribut instance dengan mengatakan self.attribute = nilai dalam metode apa pun. Misalnya __init __ ().
Lennart Regebro
@Kieveli: Dua jawaban: Anda tidak bisa, karena apa pun yang Anda tetapkan di tingkat kelas akan menjadi atribut kelas, dan setiap kejadian yang mengakses atribut itu akan mengakses atribut kelas yang sama; Anda bisa, / sortir /, dengan menggunakan propertys - yang sebenarnya adalah fungsi tingkat kelas yang bertindak seperti atribut normal tetapi menyimpan atribut dalam contoh alih-alih kelas (dengan menggunakan self.attribute = valueseperti kata Lennart).
Ethan Furman
66

Kenapa kamu tidak introspeksi?

Saya sangat terkejut tidak ada yang melakukan introspeksi mendalam yang ditawarkan oleh Python ( 2dan 3berlaku) pada callable.

Diberikan fungsi kecil yang sederhana funcdidefinisikan sebagai:

>>> def func(a = []):
...    a.append(5)

Ketika Python menemukannya, hal pertama yang akan dilakukan adalah mengkompilasinya untuk membuat codeobjek 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: daftar asekarang dapat dianggap sebagai anggota fungsi func.

Jadi, mari kita lakukan introspeksi, sebelum dan sesudahnya untuk memeriksa bagaimana daftar tersebut diperluas di dalam objek fungsi. Saya menggunakan Python 3.xuntuk ini, untuk Python 2 berlaku sama (gunakan __defaults__atau func_defaultsPython 2; ya, dua nama untuk hal yang sama).

Fungsi Sebelum Eksekusi:

>>> def func(a = []):
...     a.append(5)
...     

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):

>>> func.__defaults__
([],)

Ok, jadi daftar kosong sebagai entri tunggal __defaults__, seperti yang diharapkan.

Fungsi Setelah Eksekusi:

Sekarang mari kita jalankan fungsi ini:

>>> func()

Sekarang, mari kita lihat __defaults__lagi:

>>> func.__defaults__
([5],)

Heran? Nilai di dalam objek berubah! Panggilan berurutan ke fungsi sekarang hanya akan menambahkan listobjek tertanam :

>>> func(); func(); func()
>>> func.__defaults__
([5, 5, 5, 5],)

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 Nonesebagai default dan kemudian menginisialisasi dalam fungsi tubuh:

def func(a = None):
    # or: a = [] if a is None else a
    if a is None:
        a = []

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, funcAnda hanya dapat mengubah fungsi Anda untuk mengembalikan iddaftar yang adigunakan 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:

>>> def func(a = []): 
...     a.append(5)
...     return id(a)
>>>
>>> id(func.__defaults__[0]) == func()
True

Semua dengan kekuatan introspeksi!


* Untuk memverifikasi bahwa Python mengevaluasi argumen default selama kompilasi fungsi, coba jalankan yang berikut:

def bar(a=input('Did you just see me without calling the function?')): 
    pass  # use raw_input in Py2

seperti yang akan Anda perhatikan, input()dipanggil sebelum proses membangun fungsi dan mengikatnya dengan nama bardibuat.

Dimitris Fasarakis Hilliard
sumber
1
Apakah id(...)diperlukan untuk verifikasi terakhir itu, atau apakah isoperator akan menjawab pertanyaan yang sama?
das-g
1
@ das-g isakan baik-baik saja, saya hanya menggunakan id(val)karena saya pikir itu mungkin lebih intuitif.
Dimitris Fasarakis Hilliard
Menggunakan Nonesebagai 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.
Brilliand
58

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

def foo(arg=something_expensive_to_compute())):
    ...

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:

funcs = [ lambda i=i: i for i in range(10)]

Ini mengembalikan daftar fungsi yang mengembalikan 0,1,2,3 ... masing-masing. Jika perilaku diubah, mereka malah akan mengikat ike nilai waktu panggilan dari i, sehingga Anda akan mendapatkan daftar fungsi yang semuanya dikembalikan 9.

Satu-satunya cara untuk mengimplementasikan hal ini adalah dengan membuat penutupan lebih lanjut dengan i bound, yaitu:

def make_func(i): return lambda: i
funcs = [make_func(i) for i in range(10)]

3. Introspeksi

Pertimbangkan kodenya:

def foo(a='test', b=100, c=[]):
   print a,b,c

Kami dapat memperoleh informasi tentang argumen dan default menggunakan inspectmodul, yang

>>> inspect.getargspec(foo)
(['a', 'b', 'c'], None, None, ('test', 100, []))

Informasi ini sangat berguna untuk hal-hal seperti pembuatan dokumen, metaprogramming, dekorator dll.

Sekarang, anggap perilaku default dapat diubah sehingga ini setara dengan:

_undefined = object()  # sentinel value

def foo(a=_undefined, b=_undefined, c=_undefined)
    if a is _undefined: a='test'
    if b is _undefined: b=100
    if c is _undefined: c=[]

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.

Brian
sumber
1
Anda bisa mencapai introspeksi juga jika untuk masing-masing ada fungsi untuk membuat argumen default, bukan nilai. modul inspect hanya akan memanggil fungsi itu.
yairchu
@ SilentGhost: Saya berbicara tentang apakah perilaku diubah untuk membuatnya kembali - menciptakannya sekali adalah perilaku saat ini, dan mengapa masalah default yang bisa berubah ada.
Brian
1
@yairchu: Itu mengasumsikan konstruksi aman untuk itu (yaitu tidak memiliki efek samping). Introspeksi argumen tidak boleh melakukan apa - apa, tetapi mengevaluasi kode arbitrer bisa berakhir memiliki efek.
Brian
1
Desain bahasa yang berbeda seringkali hanya berarti menulis sesuatu secara berbeda. Contoh pertama Anda dapat dengan mudah ditulis sebagai: _price = mahal (); def foo (arg = _expensive), jika Anda secara khusus tidak ingin itu dievaluasi kembali.
Glenn Maynard
@ Glenn - itulah yang saya maksud dengan "cache variabel secara eksternal" - itu sedikit lebih verbose, dan Anda berakhir dengan variabel tambahan di namespace Anda sekalipun.
Brian
55

5 poin dalam membela Python

  1. Kesederhanaan : Perilaku ini sederhana dalam pengertian berikut: Kebanyakan orang jatuh ke dalam perangkap ini hanya sekali, tidak beberapa kali.

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

  3. 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.)

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

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

Lutz Prechelt
sumber
18
Butuh waktu setahun untuk menemukan perilaku ini mengacaukan kode saya pada produksi, akhirnya menghapus fitur lengkap sampai saya bertemu dengan cacat desain ini secara kebetulan. Saya menggunakan Django. Karena lingkungan pementasan tidak memiliki banyak permintaan, bug ini tidak pernah berdampak pada QA. Ketika kami tayang dan menerima banyak permintaan secara bersamaan - beberapa fungsi utilitas mulai menimpa parameter masing-masing! Membuat lubang keamanan, bug, dan apa yang tidak.
oriadam
7
@oriadam, jangan tersinggung, tapi saya ingin tahu bagaimana Anda belajar Python tanpa mengalami ini sebelumnya. Saya baru belajar Python sekarang dan kemungkinan perangkap ini disebutkan dalam tutorial resmi Python tepat di samping penyebutan pertama argumen default. (Seperti disebutkan dalam poin 4 dari jawaban ini.) Saya kira moralnya adalah - agak tidak simpatik - untuk membaca dokumen resmi bahasa yang Anda gunakan untuk membuat perangkat lunak produksi.
Wildcard
Juga, akan mengejutkan (bagi saya) jika suatu fungsi dengan kompleksitas yang tidak diketahui dipanggil sebagai tambahan dari pemanggilan fungsi yang saya buat.
Vatine
52

Perilaku ini mudah dijelaskan oleh:

  1. deklarasi function (class etc.) dieksekusi hanya sekali, membuat semua objek nilai default
  2. semuanya dilewatkan dengan referensi

Begitu:

def x(a=0, b=[], c=[], d=0):
    a = a + 1
    b = b + [1]
    c.append(1)
    print a, b, c
  1. a tidak berubah - setiap panggilan tugas membuat objek int baru - objek baru dicetak
  2. b tidak berubah - array baru dibuat dari nilai default dan dicetak
  3. c perubahan - operasi dilakukan pada objek yang sama - dan itu dicetak
ymv
sumber
(Sebenarnya, add adalah contoh buruk, tapi bilangan bulat menjadi berubah masih titik utama saya.)
Anon
Saya menyadarinya di chagrin saya setelah memeriksa untuk melihat bahwa, dengan b set ke [], b .__ menambahkan __ ([1]) mengembalikan [1] tetapi juga meninggalkan b masih [] meskipun daftar dapat diubah. Salahku.
Anon
@ Alan: ada __iadd__, tetapi tidak bekerja dengan int. Tentu saja. :-)
Veky
35

Yang Anda tanyakan adalah mengapa ini:

def func(a=[], b = 2):
    pass

tidak secara internal setara dengan ini:

def func(a=None, b = None):
    a_default = lambda: []
    b_default = lambda: 2
    def actual_func(a=None, b=None):
        if a is None: a = a_default()
        if b is None: b = b_default()
    return actual_func
func = func()

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.

Glenn Maynard
sumber
6
Itu tidak perlu menjadi penutup - cara yang lebih baik untuk memikirkannya hanya dengan membuat bytecode membuat default menjadi baris pertama kode - bagaimanapun Anda mengkompilasi tubuh pada saat itu - tidak ada perbedaan nyata antara kode dalam argumen dan kode di dalam tubuh.
Brian
10
Benar, tetapi itu masih akan memperlambat Python, dan itu sebenarnya akan cukup mengejutkan, kecuali jika Anda melakukan hal yang sama untuk definisi kelas, yang akan membuatnya sangat lambat karena Anda harus menjalankan ulang seluruh definisi kelas setiap kali Anda membuat instance kelas. Seperti disebutkan, perbaikannya akan lebih mengejutkan daripada masalahnya.
Lennart Regebro
Setuju dengan Lennart. Seperti yang gemar dikatakan Guido, untuk setiap fitur bahasa atau perpustakaan standar, ada seseorang di luar sana yang menggunakannya.
Jason Baker
6
Mengubahnya sekarang akan menjadi kegilaan - kita hanya mengeksplorasi mengapa memang demikian adanya. Jika memang terlambat melakukan evaluasi standar, itu tidak selalu mengejutkan. Jelas benar bahwa inti seperti perbedaan parsing akan memiliki efek menyapu, dan mungkin banyak yang tidak jelas, pada bahasa secara keseluruhan.
Glenn Maynard
35

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:

def foo(a=[]):                 # the same problematic function
    a.append(5)
    return a

>>> somevar = [1, 2]           # an example without a default parameter
>>> foo(somevar)
[1, 2, 5]
>>> somevar
[1, 2, 5]                      # usually expected [1, 2]

Solusi : salinan Solusi yang
benar-benar aman adalah untuk copyatau deepcopyobjek input pertama dan kemudian melakukan apa pun dengan salinan.

def foo(a=[]):
    a = a[:]     # a copy
    a.append(5)
    return a     # or everything safe by one line: "return a + [5]"

Banyak tipe yang bisa berubah-ubah memiliki metode salin seperti some_dict.copy()atau some_set.copy()atau dapat disalin dengan mudah seperti somelist[:]atau list(some_list). Setiap objek dapat juga disalin oleh copy.copy(any_object)atau lebih teliti copy.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. penyalinan

Contoh masalah untuk pertanyaan SO yang serupa

class Test(object):            # the original problematic class
  def __init__(self, var1=[]):
    self._var1 = var1

somevar = [1, 2]               # an example without a default parameter
t1 = Test(somevar)
t2 = Test(somevar)
t1._var1.append([1])
print somevar                  # [1, 2, [1]] but usually expected [1, 2]
print t2._var1                 # [1, 2, [1]] but usually expected [1, 2]

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

hynekcer
sumber
5
Saya harap Anda sadar bahwa Python bukan bahasa pemrograman fungsional.
Veky
6
Ya, Python adalah bahasa multi-paragigm dengan beberapa fitur fungsional. ("Jangan membuat setiap masalah terlihat seperti paku hanya karena Anda memiliki palu.") Banyak dari mereka berada dalam praktik terbaik Python. Python memiliki Pemrograman Fungsional HOWTO yang menarik . Fitur lainnya adalah penutupan dan pengerjaan currying, tidak disebutkan di sini.
hynekcer
1
Saya juga akan menambahkan, pada tahap akhir ini, bahwa semantik penugasan Python telah dirancang secara eksplisit untuk menghindari penyalinan data jika diperlukan, sehingga pembuatan salinan (dan terutama salinan dalam) akan mempengaruhi penggunaan run-time dan memori secara negatif. Karena itu mereka harus digunakan hanya jika diperlukan, tetapi pendatang baru sering mengalami kesulitan memahami kapan itu.
holdenweb
1
@ holdenweb saya setuju. Salinan sementara adalah cara yang paling umum dan kadang-kadang satu-satunya cara yang mungkin untuk melindungi data asli yang dapat berubah dari fungsi asing yang memodifikasinya secara potensial. Untungnya fungsi yang memodifikasi data secara tidak masuk akal dianggap sebagai bug dan karenanya tidak umum.
hynekcer
Saya setuju dengan jawaban ini. Dan saya tidak mengerti mengapa 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 melakukannya if a is None: a = [1, 2, 3], Anda tetap menyalin daftar itu.
koddo
30

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.

>>> def foo(a):
    a.append(5)
    print a

>>> a  = [5]
>>> foo(a)
[5, 5]
>>> foo(a)
[5, 5, 5]
>>> foo(a)
[5, 5, 5, 5]
>>> foo(a)
[5, 5, 5, 5, 5]

Tidak ada nilai default yang terlihat dalam kode ini, tetapi Anda mendapatkan masalah yang sama persis.

Masalahnya adalah bahwa fooadalah memodifikasi variabel bisa berubah berlalu dalam dari pemanggil, ketika penelepon tidak mengharapkan ini. Kode seperti ini akan baik-baik saja jika fungsinya disebut sesuatu seperti append_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 memodifikasi aapakah 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.

Ben
sumber
7
Meskipun terkait, saya pikir ini adalah perilaku yang berbeda (seperti yang kami harapkan appendakan berubah a"di tempat"). Bahwa mutable default tidak instantiated pada setiap panggilan adalah bit "tak terduga" ... setidaknya bagi saya. :)
Andy Hayden
2
@AndyHayden jika fungsi diharapkan untuk mengubah argumen, mengapa masuk akal untuk memiliki default?
Mark Ransom
@ Markarkhans satu-satunya contoh yang bisa saya pikirkan adalah cache={}. Namun, saya menduga ini "paling mencengangkan" muncul adalah ketika Anda tidak mengharapkan (atau menginginkan) fungsi yang Anda panggil untuk mengubah argumen.
Andy Hayden
1
@AndyHayden Saya meninggalkan jawaban saya sendiri di sini dengan perluasan sentimen itu. Biarkan aku tahu apa yang Anda pikirkan. Saya mungkin menambahkan contoh Anda cache={}ke dalamnya untuk kelengkapan.
Mark Ransom
1
@AndyHayden Inti dari jawaban saya adalah bahwa jika Anda pernah terkejut dengan secara tidak sengaja mengubah nilai default argumen, maka Anda memiliki bug lain, yaitu kode Anda dapat secara tidak sengaja mengubah nilai penelepon ketika default tidak digunakan. Dan perhatikan bahwa menggunakan Nonedan menetapkan default nyata jika arg None 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.
Ben
27

Sudah topik sibuk, tetapi dari apa yang saya baca di sini, berikut ini membantu saya menyadari bagaimana itu bekerja secara internal:

def bar(a=[]):
     print id(a)
     a = a + [1]
     print id(a)
     return a

>>> bar()
4484370232
4484524224
[1]
>>> bar()
4484370232
4484524152
[1]
>>> bar()
4484370232 # Never change, this is 'class property' of the function
4484523720 # Always a new object 
[1]
>>> id(bar.func_defaults[0])
4484370232
Stéphane
sumber
2
sebenarnya ini mungkin sedikit membingungkan bagi pendatang baru karena a = a + [1]kelebihan a... pertimbangkan untuk mengubahnya b = a + [1] ; print id(b)dan menambahkan baris a.append(2). Itu akan membuatnya lebih jelas bahwa +pada dua daftar selalu membuat daftar baru (ditugaskan ke b), sementara yang diubah amasih dapat memiliki yang sama id(a).
Jörn Hees
25

Ini adalah optimasi kinerja. Sebagai hasil dari fungsionalitas ini, manakah dari kedua panggilan fungsi yang menurut Anda lebih cepat?

def print_tuple(some_tuple=(1,2,3)):
    print some_tuple

print_tuple()        #1
print_tuple((1,2,3)) #2

Saya akan memberi Anda petunjuk. Inilah pembongkarannya (lihat http://docs.python.org/library/dis.html ):

#1

0 LOAD_GLOBAL              0 (print_tuple)
3 CALL_FUNCTION            0
6 POP_TOP
7 LOAD_CONST               0 (None)
10 RETURN_VALUE

#2

 0 LOAD_GLOBAL              0 (print_tuple)
 3 LOAD_CONST               4 ((1, 2, 3))
 6 CALL_FUNCTION            1
 9 POP_TOP
10 LOAD_CONST               0 (None)
13 RETURN_VALUE

Saya ragu perilaku yang berpengalaman memiliki penggunaan praktis (yang benar-benar menggunakan variabel statis dalam C, tanpa membiakkan bug?)

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

Jason Baker
sumber
24

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:

def foo(mutable_default_argument=[]): # make a list the default argument
    """function that uses a list"""

hampir persis sama dengan ini:

_a_list = [] # create a list in the globals

def foo(mutable_default_argument=_a_list): # make it the default argument
    """function that uses a list"""

del _a_list # remove globals name binding

Demonstrasi

Ini sebuah demonstrasi - Anda dapat memverifikasi bahwa mereka adalah objek yang sama setiap kali direferensikan oleh

  • melihat bahwa daftar dibuat sebelum fungsi selesai dikompilasi ke objek fungsi,
  • mengamati bahwa id sama setiap kali daftar direferensikan,
  • mengamati bahwa daftar tetap berubah ketika fungsi yang menggunakannya disebut kedua kalinya,
  • mengamati urutan di mana output dicetak dari sumber (yang saya nomorkan dengan mudah untuk Anda):

example.py

print('1. Global scope being evaluated')

def create_list():
    '''noisily create a list for usage as a kwarg'''
    l = []
    print('3. list being created and returned, id: ' + str(id(l)))
    return l

print('2. example_function about to be compiled to an object')

def example_function(default_kwarg1=create_list()):
    print('appending "a" in default default_kwarg1')
    default_kwarg1.append("a")
    print('list with id: ' + str(id(default_kwarg1)) + 
          ' - is now: ' + repr(default_kwarg1))

print('4. example_function compiled: ' + repr(example_function))


if __name__ == '__main__':
    print('5. calling example_function twice!:')
    example_function()
    example_function()

dan menjalankannya dengan python example.py:

1. Global scope being evaluated
2. example_function about to be compiled to an object
3. list being created and returned, id: 140502758808032
4. example_function compiled: <function example_function at 0x7fc9590905f0>
5. calling example_function twice!:
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a']
appending "a" in default default_kwarg1
list with id: 140502758808032 - is now: ['a', 'a']

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:

def example_function_2(default_kwarg=None):
    if default_kwarg is None:
        default_kwarg = []

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 :

Jika Anda tidak ingin default dibagi antara panggilan berikutnya, Anda dapat menulis fungsi seperti ini sebagai gantinya:

def f(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L
Aaron Hall
sumber
24

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:

def a(): return []

def b(x=a()):
    print x

Mudah-mudahan itu cukup untuk menunjukkan bahwa tidak mengeksekusi ekspresi argumen default pada waktu eksekusi defpernyataan itu tidak mudah atau tidak masuk akal, atau keduanya.

Saya setuju itu gotcha ketika Anda mencoba menggunakan konstruktor default.

Baczek
sumber
20

Solusi sederhana menggunakan Tidak Ada

>>> def bar(b, data=None):
...     data = data or []
...     data.append(b)
...     return data
... 
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3)
[3]
>>> bar(3, [34])
[34, 3]
>>> bar(3, [34])
[34, 3]
hugo24
sumber
19

Perilaku ini tidak mengejutkan jika Anda mempertimbangkan hal berikut:

  1. Perilaku atribut kelas baca-saja pada upaya penugasan, dan itu
  2. Fungsi adalah objek (dijelaskan dengan baik dalam jawaban yang diterima).

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:

... semua variabel yang ditemukan di luar lingkup terdalam hanya baca-saja ( upaya untuk menulis ke variabel seperti itu hanya akan membuat variabel lokal baru dalam cakupan terdalam, membiarkan variabel luar yang identik namanya tidak berubah ).

Lihat kembali contoh aslinya dan pertimbangkan poin-poin di atas:

def foo(a=[]):
    a.append(5)
    return a

Berikut fooadalah objek dan aatribut foo(tersedia di foo.func_defs[0]). Karena aadalah daftar, abisa 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 footanpa mengesampingkan default menggunakan nilai default dari foo.func_defs. Dalam hal ini, foo.func_defs[0]digunakan untuk adalam lingkup kode objek fungsi. Perubahan untuk aberubah foo.func_defs[0], yang merupakan bagian dari fooobjek dan berlanjut antara eksekusi kode di foo.

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:

def foo(a, L=None):
    if L is None:
        L = []
    L.append(a)
    return L

Mempertimbangkan (1) dan (2) , seseorang dapat melihat mengapa ini mencapai perilaku yang diinginkan:

  • Ketika fooobjek fungsi instantiated, foo.func_defs[0]diatur ke None, objek yang tidak dapat diubah.
  • Ketika fungsi dijalankan dengan default (tanpa parameter yang ditentukan untuk Lpanggilan fungsi), foo.func_defs[0]( None) tersedia dalam lingkup lokal sebagai L.
  • Setelah itu L = [], tugas tidak dapat berhasil foo.func_defs[0], karena atribut itu hanya baca.
  • Per (1) , variabel lokal baru juga bernama Ldibuat dalam lingkup lokal dan digunakan untuk sisa panggilan fungsi. foo.func_defs[0]dengan demikian tetap tidak berubah untuk doa di masa depan foo.
Dmitry Minkovsky
sumber
19

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 ...) :

def foo(list_arg=[5]):
    return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
# The value of 6 appended to variable 'a' is now part of the list held by 'b'.
>>> b
[5, 6, 7]  

# Although 'a' is expecting to receive 6 (the last element it appended to the list),
# it actually receives the last element appended to the shared list.
# It thus receives the value 7 previously appended by 'b'.
>>> a.pop()             
7

Anda dapat memverifikasi bahwa mereka adalah satu dan objek yang sama dengan menggunakan id:

>>> id(a)
5347866528

>>> id(b)
5347866528

Per Brett Slatkin "Python Efektif: 59 Cara Khusus untuk Menulis Python Lebih Baik", Butir 20: Gunakan Nonedan Docstrings untuk menentukan argumen default dinamis (p. 48)

Konvensi untuk mencapai hasil yang diinginkan dalam Python adalah untuk memberikan nilai default Nonedan untuk mendokumentasikan perilaku aktual dalam docstring.

Implementasi ini memastikan bahwa setiap panggilan ke fungsi menerima daftar default atau daftar yang diteruskan ke fungsi.

Metode yang disukai :

def foo(list_arg=None):
   """
   :param list_arg:  A list of input values. 
                     If none provided, used a list with a default value of 5.
   """
   if not list_arg:
       list_arg = [5]
   return list_arg

a = foo()
a.append(6)
>>> a
[5, 6]

b = foo()
b.append(7)
>>> b
[5, 7]

c = foo([10])
c.append(11)
>>> c
[10, 11]

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.

Alexander
sumber
17

Solusi di sini adalah:

  1. Gunakan Nonesebagai nilai default Anda (atau nonceobject ), dan aktifkan itu untuk membuat nilai Anda saat runtime; atau
  2. Gunakan a lambdasebagai 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)

Marcin
sumber
16

Ketika kita melakukan ini:

def foo(a=[]):
    ...

... kami memberikan argumen akepada 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?

def foo(a=pavlo):
   ...

Kapan saja, jika penelepon tidak memberi tahu kami apa yang aada, kami menggunakan kembali pavlo.

Jika pavlodapat diubah (dapat dimodifikasi), dan fooakhirnya memodifikasinya, efek yang kami perhatikan waktu berikutnya foodipanggil tanpa menentukan a.

Jadi ini yang Anda lihat (Ingat, pavlodiinisialisasi ke []):

 >>> foo()
 [5]

Sekarang, pavloadalah [5].

Memanggil foo()lagi memodifikasi pavlolagi:

>>> foo()
[5, 5]

Menentukan akapan panggilan foo()memastikan pavlotidak tersentuh.

>>> ivan = [1, 2, 3, 4]
>>> foo(a=ivan)
[1, 2, 3, 4, 5]
>>> ivan
[1, 2, 3, 4, 5]

Jadi, pavlotetap saja [5, 5].

>>> foo()
[5, 5, 5]
Saish
sumber
16

Saya terkadang mengeksploitasi perilaku ini sebagai alternatif dari pola berikut:

singleton = None

def use_singleton():
    global singleton

    if singleton is None:
        singleton = _make_singleton()

    return singleton.use_me()

Jika singletonhanya digunakan oleh use_singleton, saya suka pola berikut sebagai pengganti:

# _make_singleton() is called only once when the def is executed
def use_singleton(singleton=_make_singleton()):
    return singleton.use_me()

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.

bgreen-litl
sumber
2
Saya lebih suka menambahkan dekorator untuk memoisasi, dan meletakkan cache memoisasi ke objek fungsi itu sendiri.
Stefano Borini
Contoh ini tidak menggantikan pola yang lebih rumit yang Anda perlihatkan, karena Anda menelepon _make_singletonpada 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.
Yann Vernier
15

Anda bisa menyelesaikan ini dengan mengganti objek (dan karenanya mengikat dengan cakupan):

def foo(a=[]):
    a = list(a)
    a.append(5)
    return a

Jelek, tapi berhasil.

joedborg
sumber
3
Ini adalah solusi yang bagus jika Anda menggunakan perangkat lunak pembuatan dokumentasi otomatis untuk mendokumentasikan jenis argumen yang diharapkan oleh fungsi tersebut. Menempatkan a = Tidak Ada dan kemudian menetapkan ke [] jika a adalah Tidak ada tidak membantu pembaca memahami apa yang diharapkan.
Michael Scott Cuthbert
Gagasan keren: mem-rebinding nama itu menjamin itu tidak akan pernah bisa diubah. Saya sangat suka itu.
holdenweb
Ini persis cara untuk melakukannya. Python tidak membuat salinan parameter, jadi terserah Anda untuk membuat salinan secara eksplisit. Setelah Anda memiliki salinan, itu milik Anda untuk memodifikasi sesuka Anda tanpa efek samping yang tidak terduga.
Mark Ransom
13

Mungkin benar bahwa:

  1. Seseorang menggunakan setiap fitur bahasa / perpustakaan, dan
  2. Mengubah perilaku di sini akan keliru, tetapi

sepenuhnya konsisten untuk berpegang pada kedua fitur di atas dan masih membuat poin lain:

  1. Ini adalah fitur yang membingungkan dan sangat disayangkan di Python.

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 .

Christos Hayward
sumber
6
-1 Meskipun perspektif bisa dipertahankan, ini bukan jawaban, dan saya tidak setuju dengan itu. Terlalu banyak pengecualian khusus menghasilkan kotak sudutnya sendiri.
Marcin
3
Jadi, itu "luar biasa bodoh" untuk mengatakan bahwa dalam Python akan lebih masuk akal untuk argumen default [] untuk tetap [] setiap kali fungsi dipanggil?
Christos Hayward
3
Dan bodoh untuk menganggap sebagai idiom yang malang menetapkan argumen default untuk Tidak ada, dan kemudian di tubuh tubuh pengaturan fungsi jika argumen == Tidak ada: argumen = []? Apakah bodoh untuk menganggap idiom ini disayangkan karena sering orang menginginkan apa yang diharapkan oleh pendatang baru yang naif, bahwa jika Anda menetapkan f (argumen = []), argumen akan secara otomatis default ke nilai []?
Christos Hayward
3
Tetapi dengan Python, bagian dari semangat bahasa adalah Anda tidak perlu melakukan terlalu banyak penyelaman; array.sort () berfungsi, dan bekerja terlepas dari seberapa sedikit Anda memahami tentang penyortiran, big-O, dan konstanta. Keindahan Python dalam mekanisme penyortiran array, untuk memberikan salah satu contoh yang tak terhitung banyaknya, adalah bahwa Anda tidak diharuskan untuk menyelam lebih dalam ke internal. Dan untuk mengatakannya secara berbeda, keindahan Python adalah bahwa seseorang biasanya tidak diharuskan untuk menyelam lebih dalam ke implementasi untuk mendapatkan sesuatu yang Just Works. Dan ada solusi (... jika argumen == Tidak ada: argumen = []), GAGAL.
Christos Hayward
3
Sebagai standalone, pernyataan itu x=[]berarti "buat objek daftar kosong, dan ikat nama 'x' ke sana." Jadi, dalam def 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?
Jerry B
10

Ini bukan cacat desain . Siapa pun yang tersandung ini melakukan kesalahan.

Ada 3 kasus yang saya lihat di mana Anda mungkin mengalami masalah ini:

  1. Anda bermaksud memodifikasi argumen sebagai efek samping dari fungsi. Dalam hal ini tidak masuk akal untuk memiliki argumen default. Satu-satunya pengecualian adalah ketika Anda menyalahgunakan daftar argumen untuk memiliki atribut fungsi, misalnya cache={}, dan Anda tidak akan diharapkan untuk memanggil fungsi dengan argumen yang sebenarnya sama sekali.
  2. Anda bermaksud membiarkan argumen tidak diubah , tetapi Anda tidak sengaja mengubahnya. Itu bug, perbaiki.
  3. Anda bermaksud memodifikasi argumen untuk digunakan di dalam fungsi, tetapi tidak mengharapkan modifikasi dapat dilihat di luar fungsi. Dalam hal ini Anda perlu membuat salinan argumen, apakah itu default atau tidak! Python bukan bahasa panggilan-oleh-nilai sehingga tidak membuat salinan untuk Anda, Anda harus eksplisit tentang hal itu.

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.

Mark tebusan
sumber
"Melakukan sesuatu yang salah" adalah diagnosis. Yang mengatakan, saya pikir ada saatnya = Tidak ada pola yang berguna, tetapi umumnya Anda tidak ingin memodifikasi jika melewati yang bisa berubah dalam kasus itu (2). The cache={}Pola benar-benar sebuah solusi wawancara-satunya, dalam kode nyata Anda mungkin ingin @lru_cache!
Andy Hayden
9

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

def example(errors=[]):
    # statements
    # Something went wrong
    mistake = True
    if mistake:
        tryToFixIt(errors)
        # Didn't work.. let's try again
        tryToFixItAnotherway(errors)
        # This time it worked
    return errors

def tryToFixIt(err):
    err.append('Attempt to fix it')

def tryToFixItAnotherway(err):
    err.append('Attempt to fix it by another way')

def main():
    for item in range(2):
        errors = example()
    print '\n'.join(errors)

main()

mencetak yang berikut ini

Attempt to fix it
Attempt to fix it by another way
Attempt to fix it
Attempt to fix it by another way
Norfeldt
sumber
8

Ubah saja fungsi menjadi:

def notastonishinganymore(a = []): 
    '''The name is just a joke :)'''
    a = a[:]
    a.append(5)
    return a
ytpillai
sumber
7

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, ahanya 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. adievaluasi 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 ========

def foo(x, items=None):
    if items is None:
        items = []
    items.append(x)
    return items

foo(1)  #return [1]
foo(2)  #return [2]
foo(3)  #return [3]

[]adalah sebuah objek, demikian juga None(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 =======

def foo(x, items=[]):
    items.append(x)
    return items

foo(1)    # returns [1]
foo(2,[]) # returns [2]
foo(3)    # returns [1,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, itemsharus mengambil alamat yang baru ini[] , katakan 2222222, dan mengembalikannya setelah melakukan beberapa perubahan. Sekarang foo (3) dieksekusi. sejak itu sajaxdisediakan, 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 menghasilkan items[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:

for i in range(10):
    def callback():
        print "clicked button", i
    UI.Button("button %s" % i, callback)

Setiap tombol dapat memiliki fungsi panggilan balik yang berbeda yang akan menampilkan nilai yang berbeda i. Saya dapat memberikan contoh untuk menunjukkan ini:

x=[]
for i in range(10):
    def callback():
        print(i)
    x.append(callback) 

Jika kita mengeksekusi x[7]()kita akan mendapatkan 7 seperti yang diharapkan, dan x[9]()akan memberi 9, nilai lain dari i.

pengguna2384994
sumber
5
Poin terakhir Anda salah. Cobalah dan Anda akan melihat bahwa x[7]()adalah 9.
Duncan
2
"python meneruskan tipe data dasar dengan nilai, yaitu, membuat salinan lokal dari nilai ke variabel lokal" sama sekali tidak benar. Saya heran bahwa seseorang jelas dapat mengenal Python dengan sangat baik, namun memiliki kesalahpahaman mendasar yang begitu mengerikan. :-(
Veky
6

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:

...                           # defining scope
def name(parameter=default):  # ???
    ...                       # execution scope

The def namebagian harus mengevaluasi dalam ruang lingkup mendefinisikan - kami ingin namemenjadi tersedia di sana, setelah semua. Mengevaluasi fungsi hanya di dalam dirinya sendiri akan membuatnya tidak dapat diakses.

Karena parameterini adalah nama yang konstan, kita dapat "mengevaluasinya" bersamaan dengan def name. Ini juga memiliki kelebihan yang menghasilkan fungsi dengan tanda tangan yang dikenal sebagai name(parameter=...):, bukannya telanjang name(...):.

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 defaultdievaluasi pada waktu definisi, itu masih dapat mempengaruhi waktu eksekusi. Jika defaultdievaluasi pada waktu pelaksanaan, itu tidak dapat mempengaruhi waktu definisi. Memilih "pada definisi" memungkinkan mengekspresikan kedua kasus, sementara memilih "saat eksekusi" hanya dapat mengungkapkan satu:

def name(parameter=defined):  # set default at definition time
    ...

def name(parameter=default):     # delay default until execution time
    parameter = default if parameter is None else parameter
    ...
MisterMiyagi
sumber
"Konsistensi sudah mengatakan" pada definisi ": segala sesuatu 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. defaultadalah hal yang berbeda dari yang lainnya: itu ekspresi. Mengevaluasi suatu ekspresi adalah proses yang sangat berbeda dari mendefinisikan suatu fungsi.
LarsH
@LarsH Fungsi definisi yang sedang dievaluasi dengan Python. Apakah itu dari pernyataan ( 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.
MisterMiyagi
OK, membuat fungsi berarti evaluasi dalam beberapa hal, tetapi jelas tidak dalam arti bahwa setiap ekspresi di dalamnya dievaluasi pada saat definisi. Sebagian besar tidak. Tidak jelas bagi saya dalam arti apa tanda tangan itu terutama "dievaluasi" pada waktu definisi lebih dari fungsi tubuh "dievaluasi" (diuraikan menjadi representasi yang sesuai); sedangkan ekspresi dalam fungsi tubuh jelas tidak dievaluasi dalam arti penuh. Dari sudut pandang ini, konsistensi akan mengatakan bahwa ekspresi dalam tanda tangan tidak boleh "sepenuhnya" dievaluasi juga.
LarsH
Saya tidak bermaksud bahwa Anda salah, hanya saja kesimpulan Anda tidak berdasarkan dari konsistensi saja.
LarsH
@ LarsH Default bukan bagian dari tubuh, saya juga tidak mengklaim bahwa konsistensi adalah satu-satunya kriteria. Bisakah Anda memberi saran bagaimana menjelaskan jawabannya?
MisterMiyagi
3

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.

import inspect
from copy import copy

def sanify(function):
    def wrapper(*a, **kw):
        # store the default values
        defaults = inspect.getargspec(function).defaults # for python2
        # construct a new argument list
        new_args = []
        for i, arg in enumerate(defaults):
            # allow passing positional arguments
            if i in range(len(a)):
                new_args.append(a[i])
            else:
                # copy the value
                new_args.append(copy(arg))
        return function(*new_args, **kw)
    return wrapper

Sekarang mari kita mendefinisikan kembali fungsi kita menggunakan dekorator ini:

@sanify
def foo(a=[]):
    a.append(5)
    return a

foo() # '[5]'
foo() # '[5]' -- as desired

Ini sangat rapi untuk fungsi yang mengambil banyak argumen. Membandingkan:

# the 'correct' approach
def bar(a=None, b=None, c=None):
    if a is None:
        a = []
    if b is None:
        b = []
    if c is None:
        c = []
    # finally do the actual work

dengan

# the nasty decorator hack
@sanify
def bar(a=[], b=[], c=[]):
    # wow, works right out of the box!

Penting untuk dicatat bahwa solusi di atas rusak jika Anda mencoba menggunakan kata kunci args, seperti:

foo(a=[4])

Penghias dapat disesuaikan untuk memungkinkan untuk itu, tetapi kami meninggalkan ini sebagai latihan untuk pembaca;)

Przemek D
sumber