Argumen default yang dapat diubah Python: Mengapa?


20

Saya tahu bahwa argumen default dibuat pada waktu inisialisasi fungsi dan tidak setiap kali fungsi dipanggil. Lihat kode berikut:

def ook (item, lst=[]):
    lst.append(item)
    print 'ook', lst

def eek (item, lst=None):
    if lst is None: lst = []
    lst.append(item)
    print 'eek', lst

max = 3
for x in xrange(max):
    ook(x)

for x in xrange(max):
    eek(x)

Yang tidak saya dapatkan adalah mengapa ini diterapkan dengan cara ini. Apa manfaat yang ditawarkan perilaku ini daripada inisialisasi pada setiap waktu panggilan?


Hal ini sudah dibahas di mencengangkan detail pada Stack Overflow: stackoverflow.com/q/1132941/5419599
Wildcard

Jawaban:


14

Saya pikir alasannya adalah kesederhanaan implementasi. Biarkan saya uraikan.

Nilai default dari fungsi adalah ekspresi yang perlu Anda evaluasi. Dalam kasus Anda, ini adalah ekspresi sederhana yang tidak bergantung pada penutupan, tetapi bisa berupa sesuatu yang berisi variabel bebas - def ook(item, lst = something.defaultList()). Jika Anda mendesain Python, Anda akan memiliki pilihan - apakah Anda mengevaluasinya sekali ketika fungsi didefinisikan atau setiap kali ketika fungsi dipanggil. Python memilih yang pertama (tidak seperti Ruby, yang cocok dengan opsi kedua).

Ada beberapa manfaat untuk ini.

Pertama, Anda mendapatkan beberapa peningkatan kecepatan dan memori. Dalam kebanyakan kasus, Anda akan memiliki argumen default yang tidak dapat diubah dan Python dapat membangunnya hanya sekali, alih-alih pada setiap pemanggilan fungsi. Ini menghemat (sebagian) memori dan waktu. Tentu saja, itu tidak bekerja dengan baik dengan nilai-nilai yang bisa berubah, tetapi Anda tahu bagaimana Anda bisa berkeliling.

Manfaat lain adalah kesederhanaan. Sangat mudah untuk memahami bagaimana ekspresi dievaluasi - ia menggunakan ruang lingkup leksikal ketika fungsi didefinisikan. Jika mereka pergi ke arah lain, ruang lingkup leksikal mungkin berubah antara definisi dan doa dan membuatnya sedikit lebih sulit untuk di-debug. Python akan sangat mudah dalam kasus-kasus tersebut.


3
Poin menarik - meskipun itu biasanya prinsip paling tidak mengejutkan dengan Python. Beberapa hal sederhana dalam pengertian kompleksitas model formal, namun tidak jelas dan mengejutkan, dan saya pikir ini penting.
Steve314

1
Satu hal yang paling tidak mengejutkan di sini adalah ini: jika Anda memiliki semantik evaluasi-pada-setiap-panggilan, Anda bisa mendapatkan kesalahan jika penutupan berubah di antara dua panggilan fungsi (yang sangat mungkin). Ini bisa lebih mengejutkan dibandingkan dengan mengetahui bahwa itu dievaluasi sekali. Tentu saja, orang dapat berargumen bahwa ketika Anda berasal dari bahasa lain, Anda kecuali mengevaluasi semantik setiap panggilan dan itu adalah kejutan, tetapi Anda dapat melihat bagaimana kelanjutannya :)
Stefan Kanev

Poin bagus tentang ruang lingkup
0xc0de

Saya pikir ruang lingkup sebenarnya lebih penting. Karena Anda tidak terbatas pada konstanta untuk default, Anda mungkin memerlukan variabel yang tidak dalam cakupan di situs panggilan.
Mark Ransom

5

Salah satu cara untuk mengatakannya adalah bahwa parameter lst.append(item) tidak bermutasi lst. lstmasih referensi daftar yang sama. Hanya saja isi dari daftar itu telah dimutasi.

Pada dasarnya, Python tidak memiliki (yang saya ingat) variabel konstan atau tidak berubah sama sekali - tetapi ia memiliki beberapa tipe konstan dan tidak dapat diubah. Anda tidak dapat mengubah nilai integer, Anda hanya bisa menggantinya. Tetapi Anda dapat memodifikasi konten daftar tanpa menggantinya.

Seperti integer, Anda tidak dapat mengubah referensi, Anda hanya bisa menggantinya. Tetapi Anda dapat memodifikasi konten objek yang direferensikan.

Sedangkan untuk membuat objek default sekali, saya membayangkan itu sebagian besar sebagai optimasi, untuk menghemat overhead pembuatan objek dan pengumpulan sampah.


+1 Tepat. Sangat penting untuk memahami lapisan tipuan - bahwa variabel bukan nilai; alih-alih referensi nilai. Untuk mengubah variabel, nilainya dapat ditukar , atau bermutasi (jika itu bisa berubah).
Joonas Pulakka

Ketika dihadapkan dengan sesuatu yang rumit yang melibatkan variabel dalam python, saya merasa perlu untuk mempertimbangkan "=" sebagai "operator pengikat nama"; namanya selalu melambung, apakah benda yang kita ikat itu baru (objek baru atau jenis instance yang tidak dapat diubah) atau tidak.
StarWeaver

4

Apa manfaat yang ditawarkan perilaku ini daripada inisialisasi pada setiap waktu panggilan?

Ini memungkinkan Anda memilih perilaku yang Anda inginkan, seperti yang Anda tunjukkan dalam contoh Anda. Jadi jika Anda ingin argumen default tidak dapat diubah, Anda menggunakan nilai yang tidak dapat diubah , seperti Noneatau 1. Jika Anda ingin membuat argumen default bisa berubah, Anda menggunakan sesuatu yang bisa berubah, seperti []. Itu hanya fleksibilitas, meskipun harus diakui, itu bisa menggigit jika Anda tidak mengetahuinya.


2

Saya pikir jawaban sebenarnya adalah: Python ditulis sebagai bahasa prosedural dan hanya mengadopsi aspek fungsional setelah-fakta. Apa yang Anda cari adalah untuk default parameter yang harus dilakukan sebagai closure, dan closure di Python benar-benar hanya setengah matang. Untuk bukti coba ini:

a = []
for i in range(3):
    a.append(lambda: i)
print [ f() for f in a ]

yang memberi di [2, 2, 2]mana Anda akan mengharapkan penutupan sejati untuk menghasilkan [0, 1, 2].

Ada cukup banyak hal yang saya suka jika Python memiliki kemampuan untuk membungkus parameter defaulting di penutupan. Sebagai contoh:

def foo(a, b=a.b):
    ...

Akan sangat berguna, tetapi "a" tidak berada dalam ruang lingkup pada waktu definisi fungsi, jadi Anda tidak dapat melakukan itu dan sebaliknya harus melakukan dengan kikuk:

def foo(a, b=None):
    if b is None:
        b = a.b

Yang merupakan hal yang hampir sama ... hampir.



1

Manfaat yang sangat besar adalah memoisasi. Ini adalah contoh standar:

def fibmem(a, cache={0:1,1:1}):
    if a in cache: return cache[a]
    res = fib(a-1, cache) + fib(a-2, cache)
    cache[a] = res
    return res

dan untuk perbandingan:

def fib(a):
    if a == 0 or a == 1: return 1
    return fib(a-1) + fib(a-2)

Pengukuran waktu dalam ipython:

In [43]: %time print(fibmem(33))
5702887
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 200 µs

In [43]: %time print(fib(33))
5702887
CPU times: user 1.44 s, sys: 15.6 ms, total: 1.45 s
Wall time: 1.43 s

0

Itu terjadi karena kompilasi dengan Python dilakukan dengan mengeksekusi kode deskriptif.

Kalau ada yang bilang

def f(x = {}):
    ....

akan sangat jelas bahwa Anda menginginkan array baru setiap kali.

Tetapi bagaimana jika saya katakan:

list_of_all = {}
def create(stuff, x = list_of_all):
    ...

Di sini saya kira saya ingin membuat barang ke berbagai daftar, dan memiliki global catch-all ketika saya tidak menentukan daftar.

Tapi bagaimana kompiler akan menebak ini? Jadi mengapa mencoba? Kita bisa mengandalkan apakah ini dinamai atau tidak, dan itu kadang-kadang bisa membantu, tetapi sebenarnya itu hanya dugaan saja. Pada saat yang sama, ada alasan bagus untuk tidak mencoba - konsistensi.

Seperti itu, Python hanya menjalankan kode. Variabel list_of_all sudah diberi objek, sehingga objek dilewatkan dengan referensi ke dalam kode yang default x dengan cara yang sama bahwa panggilan ke fungsi apa pun akan mendapatkan referensi ke objek lokal yang disebutkan di sini.

Jika kita ingin membedakan yang tidak disebutkan namanya dari case yang disebutkan, itu akan melibatkan kode pada kompilasi yang menjalankan tugas dengan cara yang sangat berbeda dari yang dijalankan saat run-time. Jadi kami tidak membuat kasus khusus.


-5

Ini terjadi karena fungsi dalam Python adalah objek kelas satu :

Nilai parameter default dievaluasi 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 .

Selanjutnya dijelaskan bahwa mengedit nilai parameter memodifikasi nilai default untuk panggilan berikutnya, dan bahwa solusi sederhana menggunakan Tidak Ada sebagai default, dengan tes eksplisit di fungsi tubuh, adalah semua yang diperlukan untuk memastikan tidak ada kejutan.

Yang berarti def foo(l=[])menjadi instance dari fungsi itu ketika dipanggil, dan akan digunakan kembali untuk panggilan lebih lanjut. Pikirkan parameter fungsi sebagai terpisah dari atribut objek.

Pro's dapat mencakup pengungkitan ini agar kelas memiliki variabel statis C-like. Jadi yang terbaik adalah mendeklarasikan nilai default Tidak Ada dan menginisialisasi mereka sesuai kebutuhan:

class Foo(object):
    def bar(self, l=None):
        if not l:
            l = []
        l.append(5)
        return l

f = Foo()
print(f.bar())
print(f.bar())

g = Foo()
print(g.bar())
print(g.bar())

hasil:

[5] [5] [5] [5]

bukannya yang tak terduga:

[5] [5, 5] [5, 5, 5] [5, 5, 5, 5]


5
Tidak. Anda dapat mendefinisikan fungsi (kelas satu atau tidak) secara berbeda untuk mengevaluasi kembali ekspresi argumen default untuk setiap panggilan. Dan segala sesuatu setelah itu, yaitu sekitar 90% dari jawabannya, benar-benar di samping pertanyaan. -1

1
Jadi, bagikan kepada kami pengetahuan ini tentang cara mendefinisikan fungsi untuk mengevaluasi arg default untuk setiap panggilan, saya ingin mengetahui cara yang lebih sederhana daripada yang direkomendasikan oleh Python Docs .
balikkan

2
Pada tingkat desain bahasa yang saya maksud. Definisi bahasa Python saat ini menyatakan bahwa argumen default diperlakukan sebagaimana adanya; bisa juga menyatakan bahwa argumen default diperlakukan dengan cara lain. TKI Anda menjawab "begitulah" pada pertanyaan "mengapa hal-hal seperti itu".

Python bisa menerapkan parameter default yang mirip dengan bagaimana Coffeescript melakukannya. Itu akan memasukkan bytecode untuk memeriksa parameter yang hilang, dan jika mereka hilang akan mengevaluasi ekspresi.
Winston Ewert
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.