Pertanyaan "Least Astonishment" dan Argumen Mutable Default


Siapa pun yang mengotak-atik Python cukup lama telah digigit (atau robek-robek) oleh masalah berikut:

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

Para pemula Python akan mengharapkan fungsi ini untuk selalu mengembalikan daftar hanya dengan satu elemen: [5]. Hasilnya malah sangat berbeda, dan sangat mencengangkan (bagi pemula):

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

Seorang manajer saya pernah mengadakan pertemuan pertamanya dengan fitur ini, dan menyebutnya "cacat desain dramatis" dari bahasa tersebut. Saya menjawab bahwa perilaku tersebut memiliki penjelasan yang mendasarinya, dan itu memang sangat membingungkan dan tidak terduga jika Anda tidak memahami internal. Namun, saya tidak dapat menjawab (kepada 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 relatif ke mana harus meletakkan ruang lingkup parameter: di dalam fungsi atau "bersama" dengan itu?

Melakukan pengikatan di dalam fungsi berarti itu x secara efektif terikat ke default yang ditentukan ketika fungsi dipanggil, tidak didefinisikan, sesuatu yang akan menyajikan kesalahan yang dalam: def garis akan "hibrida" dalam arti bahwa bagian dari pengikatan (dari objek fungsi) akan terjadi pada definisi, dan bagian (penugasan parameter default) pada waktu pemanggilan fungsi.

Perilaku yang sebenarnya lebih konsisten: semua garis yang dievaluasi ketika garis dijalankan, yang berarti pada definisi fungsi.


2049
2017-07-15 18:00


asal


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 bukan hanya sepotong kode.

Segera setelah Anda berpikir dengan cara ini, maka itu benar-benar masuk akal: suatu fungsi adalah objek yang dievaluasi pada definisinya; parameter default adalah jenis "data anggota" dan karena itu statusnya dapat berubah dari satu panggilan ke yang lain - persis seperti pada objek lainnya.

Bagaimanapun, Effbot memiliki penjelasan yang sangat bagus tentang alasan perilaku ini Nilai Parameter Default dengan Python.
Saya menemukan itu sangat jelas, dan saya benar-benar menyarankan membacanya untuk pengetahuan yang lebih baik tentang bagaimana benda berfungsi berfungsi.


1349
2017-07-17 21:29



Misalkan Anda memiliki kode berikut

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

def eat(food=fruits):
    ...

Ketika saya melihat deklarasi makan, hal yang paling tidak mengherankan adalah berpikir bahwa jika parameter pertama tidak diberikan, maka itu akan sama dengan tupel ("apples", "bananas", "loganberries")

Namun, seharusnya nanti di kode, saya melakukan sesuatu seperti

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

maka jika parameter standar 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 Anda foofungsi di atas telah memutasi daftar.

Masalah sebenarnya terletak pada variabel yang bisa berubah, dan semua bahasa memiliki masalah ini sampai batas tertentu. Inilah pertanyaannya: misalkan di Java 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 dari StringBuffer kunci ketika ditempatkan ke peta, atau apakah itu menyimpan kunci dengan referensi? Either way, seseorang tercengang; baik orang yang mencoba mengeluarkan objek dari Map menggunakan nilai yang identik dengan yang mereka gunakan, 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 (ini adalah sebenarnya mengapa Python tidak mengizinkan tipe data built-in yang bisa berubah menjadi digunakan sebagai tombol kamus).

Contoh Anda adalah contoh yang bagus dari kasus di mana pendatang baru Python akan terkejut dan tergigit. Tapi saya berpendapat bahwa jika kita "memperbaiki" ini, maka itu hanya akan menciptakan situasi yang berbeda di mana mereka akan digigit, dan itu akan menjadi kurang intuitif. Selain itu, ini selalu terjadi ketika berhadapan dengan variabel yang bisa berubah; Anda selalu mengalami kasus di mana seseorang dapat secara intuitif 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 dapat melakukan kasus khusus menggunakan daftar yang kosong, tetapi jenis casing khusus itu akan menimbulkan kekagetan, belum lagi tidak kompatibel.


231
2017-07-15 18:11



AFAICS belum ada yang memposting bagian yang relevan dari dokumentasi:

Nilai parameter default dievaluasi ketika definisi fungsi dijalankan. Ini berarti bahwa ekspresi dievaluasi sekali, ketika fungsi didefinisikan, dan bahwa nilai "pra-dihitung" yang sama digunakan untuk setiap panggilan. Hal ini sangat penting untuk dipahami ketika parameter default adalah objek yang dapat berubah, seperti daftar atau kamus: jika fungsi memodifikasi objek (misalnya dengan menambahkan item ke daftar), nilai default akan berlaku dimodifikasi. Ini umumnya bukan apa yang dimaksudkan. Cara di sekitar ini adalah dengan menggunakan None sebagai default, dan secara eksplisit menguji untuk itu di dalam tubuh fungsi [...]


195
2017-07-10 14:50



Saya tidak tahu apa-apa tentang cara kerja interpreter Python (dan saya bukan ahli dalam compiler dan interpreter) 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 argumen argumen default. Saat Anda memberi contoh daftar:

a = []

Anda berharap mendapatkan baru daftar yang direferensikan oleh Sebuah.

Mengapa a = [] di

def x(a=[]):

instantiate daftar baru pada definisi fungsi dan bukan pada permintaan? Ini seperti Anda bertanya "jika pengguna tidak memberikan argumen itu memberi contoh daftar baru dan menggunakannya seolah-olah dihasilkan oleh penelepon ". Saya pikir ini ambigu sebagai gantinya:

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

pengguna, yang Anda inginkan Sebuah ke default ke datetime yang terkait dengan ketika Anda mendefinisikan atau mengeksekusi x? Dalam hal ini, seperti pada yang sebelumnya, saya akan menjaga perilaku yang sama seolah-olah argumen default "tugas" adalah instruksi pertama dari fungsi (datetime.now () dipanggil pada pemanggilan fungsi). Di sisi lain, jika pengguna menginginkan pemetaan definisi-waktu dia bisa menulis:

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

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

def x(static a=b):

97
2017-07-15 23:21



Nah, alasannya cukup sederhana bahwa bindings dilakukan ketika kode dijalankan, dan definisi fungsi dijalankan, baik ... ketika fungsi didefinisikan.

Bandingkan ini:

class BananaBunch:
    bananas = []

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

Kode ini menderita dari kejadian kebetulan yang tidak terduga yang sama. Pisang adalah atribut kelas, dan karenanya, ketika Anda menambahkan sesuatu ke dalamnya, itu ditambahkan ke semua contoh kelas itu. Alasannya persis sama.

Ini hanya "Cara Kerjanya", dan membuatnya bekerja secara berbeda dalam kasus fungsi mungkin akan rumit, dan dalam kasus kelas kemungkinan tidak mungkin, atau setidaknya memperlambat objek Instansiasi banyak, karena Anda harus menjaga kode kelas di sekitar dan jalankan ketika objek dibuat.

Ya, itu tidak terduga. Tapi begitu penny jatuh, itu sangat cocok dengan bagaimana Python bekerja secara umum. Sebenarnya, ini adalah alat bantu mengajar yang baik, dan setelah Anda mengerti mengapa ini terjadi, Anda akan grok python jauh lebih baik.

Yang mengatakan itu harus tampil menonjol dalam tutorial Python yang baik. Karena seperti yang Anda sebutkan, semua orang mengalami masalah ini cepat atau lambat.


72
2017-07-15 18:54



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 tidak ada gunanya untuk mencegah kebingungan newbie. Kerugian melakukannya adalah:

1. Kinerja

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

Jika evaluasi waktu panggilan digunakan, maka fungsi yang mahal disebut setiap kali fungsi Anda digunakan tanpa argumen. Anda dapat membayar harga yang mahal untuk setiap panggilan, atau perlu secara manual menyimpan nilai eksternal, mencemari ruang nama Anda, dan menambahkan verbositas.

2. Memaksa parameter terikat

Trik yang berguna adalah untuk mengikat parameter lambda ke arus 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 berubah, mereka malah akan mengikat i ke waktu panggilan nilai saya, sehingga Anda akan mendapatkan daftar fungsi yang semuanya kembali 9.

Satu-satunya cara untuk menerapkan ini jika tidak akan membuat penutupan lebih lanjut dengan i terikat, 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

Kita bisa mendapatkan informasi tentang argumen dan default menggunakan inspect modul, yang mana

>>> 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, kami kehilangan kemampuan untuk melakukan introspeksi, dan melihat apa argumen defaultnya adalah. Karena benda-benda belum dikonstruksi, kita tidak akan pernah dapat menangkapnya tanpa benar-benar memanggil fungsi. Yang terbaik yang bisa kita lakukan adalah menyimpan kode sumber dan mengembalikannya sebagai string.


50
2017-07-16 10:05



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 adalah, jelas, bagian dari fungsi heading (bukan badan fungsi). Oleh karena itu harus dievaluasi pada waktu buka modul (dan hanya pada waktu buka modul, kecuali bersarang), tidak pada waktu panggilan fungsi.

  3. Kegunaan: Seperti yang ditunjukkan Frederik Lundh dalam penjelasannya dari "Nilai Parameter Default dengan Python", yang perilaku saat ini bisa sangat berguna untuk pemrograman tingkat lanjut. (Gunakan dengan hemat.)

  4. Dokumentasi yang memadai: Dalam dokumentasi Python paling dasar, tutorial, masalah ini dengan keras diumumkan sebagai sebuah "Peringatan penting" dalam pertama subbagian Bagian "Lebih lanjut tentang Mendefinisikan Fungsi". Peringatan itu bahkan menggunakan boldface, yang jarang diterapkan di luar pos. RTFM: Baca panduan halus.

  5. Meta-learning: Jatuh ke dalam perangkap sebenarnya sangat momen membantu (setidaknya jika Anda adalah seorang pembelajar reflektif), karena Anda selanjutnya akan lebih memahami maksudnya "Konsistensi" di atas dan kehendak itu mengajari Anda banyak tentang Python.


47
2018-03-30 11:18