Python (dan Python C API): __new__ versus __init__

126

Pertanyaan yang akan saya tanyakan tampaknya merupakan duplikat dari penggunaan Python untuk __new__ dan __init__? , tetapi terlepas dari itu, masih belum jelas bagi saya apa perbedaan praktis antara __new__dan __init__.

Sebelum Anda buru-buru memberi tahu saya bahwa __new__ini untuk membuat objek dan __init__untuk menginisialisasi objek, izinkan saya menjelaskan: Saya mengerti. Pada kenyataannya, perbedaan itu cukup alami bagi saya, karena saya memiliki pengalaman dalam C ++ di mana kami memiliki penempatan baru , yang juga memisahkan alokasi objek dari inisialisasi.

The Python C API tutorial menjelaskan seperti ini:

Anggota baru bertanggung jawab untuk membuat (bukan menginisialisasi) objek jenis. Itu diekspos dalam Python sebagai __new__()metode. ... Salah satu alasan untuk menerapkan metode baru adalah untuk memastikan nilai awal variabel instan .

Jadi, ya - saya mendapatkan apa yang __new__dilakukan, tetapi meskipun demikian, saya masih tidak mengerti mengapa ini berguna dalam Python. Contoh yang diberikan mengatakan bahwa __new__mungkin berguna jika Anda ingin "memastikan nilai awal variabel instan". Nah, bukankah itu tepatnya yang __init__akan dilakukan?

Dalam tutorial C API, contoh ditampilkan di mana Tipe baru (disebut "Noddy") dibuat, dan fungsi Type __new__didefinisikan. Tipe Noddy berisi anggota string yang disebut first, dan anggota string ini diinisialisasi ke string kosong seperti:

static PyObject * Noddy_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    .....

    self->first = PyString_FromString("");
    if (self->first == NULL)
    {
       Py_DECREF(self);
       return NULL;
    }

    .....
}

Perhatikan bahwa tanpa __new__metode yang didefinisikan di sini, kita harus menggunakan PyType_GenericNew, yang hanya menginisialisasi semua anggota variabel instan ke NULL. Jadi satu-satunya manfaat dari __new__metode ini adalah bahwa variabel instance akan mulai sebagai string kosong, sebagai lawan dari NULL. Tetapi mengapa ini pernah berguna, karena jika kita peduli untuk memastikan variabel instan kita diinisialisasi ke beberapa nilai default, kita bisa saja melakukannya dalam __init__metode ini?

Channel72
sumber

Jawaban:

137

Perbedaannya terutama muncul dengan tipe yang bisa berubah vs tidak berubah.

__new__menerima jenis sebagai argumen pertama, dan (biasanya) mengembalikan contoh baru dari jenis itu. Dengan demikian sangat cocok untuk digunakan dengan tipe yang bisa berubah dan tidak berubah.

__init__menerima sebuah instance sebagai argumen pertama dan memodifikasi atribut dari instance itu. Ini tidak pantas untuk tipe yang tidak dapat diubah, karena akan memungkinkan mereka untuk dimodifikasi setelah pembuatan dengan meneleponobj.__init__(*args) .

Bandingkan perilaku tupledan list:

>>> x = (1, 2)
>>> x
(1, 2)
>>> x.__init__([3, 4])
>>> x # tuple.__init__ does nothing
(1, 2)
>>> y = [1, 2]
>>> y
[1, 2]
>>> y.__init__([3, 4])
>>> y # list.__init__ reinitialises the object
[3, 4]

Adapun mengapa mereka terpisah (selain dari alasan historis sederhana): __new__metode memerlukan sekelompok boilerplate untuk mendapatkan yang benar (penciptaan objek awal, dan kemudian mengingat untuk mengembalikan objek di akhir). __init__metode, sebaliknya, sangat sederhana, karena Anda baru saja menetapkan atribut apa pun yang perlu Anda atur.

Selain __init__metode yang lebih mudah untuk ditulis, dan perbedaan yang bisa berubah vs tidak berubah yang disebutkan di atas, pemisahan juga dapat dieksploitasi untuk membuat memanggil kelas induk __init__dalam subkelas opsional dengan mengatur setiap contoh invarian yang benar-benar diperlukan di __new__. Ini umumnya merupakan praktik yang meragukan - biasanya lebih jelas untuk memanggil __init__metode kelas induk jika diperlukan.

ncoghlan
sumber
1
kode yang Anda sebut "boilerplate" di __new__bukan boilerplate, karena boilerplate tidak pernah berubah. Terkadang Anda perlu mengganti kode tertentu dengan sesuatu yang berbeda.
Miles Rout
13
Membuat, atau mendapatkan, instance (biasanya dengan superpanggilan) dan mengembalikan instance adalah bagian penting dari setiap __new__implementasi, dan "boilerplate" yang saya maksud. Sebaliknya, passadalah implementasi yang valid untuk __init__- tidak ada perilaku yang diperlukan sama sekali.
ncoghlan
37

Mungkin ada kegunaan lain untuk __new__tetapi ada satu yang benar-benar jelas: Anda tidak dapat mensubklasifikasikan tipe abadi tanpa menggunakan __new__. Jadi misalnya, katakan Anda ingin membuat subkelas tupel yang hanya dapat berisi nilai integral antara 0 dan size.

class ModularTuple(tuple):
    def __new__(cls, tup, size=100):
        tup = (int(x) % size for x in tup)
        return super(ModularTuple, cls).__new__(cls, tup)

Anda tidak dapat melakukan ini dengan __init__- jika Anda mencoba untuk memodifikasi selfdalam __init__, penafsir akan mengeluh bahwa Anda sedang berusaha untuk memodifikasi objek abadi.

pengirim
sumber
1
Saya tidak mengerti mengapa kita harus menggunakan super? Maksudku mengapa harus baru mengembalikan sebuah instance dari superclass? Selain itu, seperti yang Anda katakan, mengapa kita harus memberikan cl secara eksplisit ke yang baru ? super (ModularTuple, cls) tidak mengembalikan metode terikat?
Alcott
3
@Alcott, saya pikir Anda salah memahami perilaku __new__. Kami memberikan clssecara eksplisit kepada __new__karena, seperti yang Anda baca di sini __new__ selalu memerlukan jenis sebagai argumen pertama. Kemudian mengembalikan sebuah instance dari tipe itu. Jadi kami tidak mengembalikan instance dari superclass - kami mengembalikan instance cls. Dalam hal ini, itu sama seperti jika kita katakan tuple.__new__(ModularTuple, tup).
pengirim
35

__new__()dapat mengembalikan objek tipe selain dari kelas yang terikat padanya. __init__()hanya menginisialisasi instance kelas yang ada.

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5
Ignacio Vazquez-Abrams
sumber
Ini adalah penjelasan paling ramping sejauh ini.
Tarik
Tidak sepenuhnya benar. Saya punya __init__metode yang berisi kode yang mirip self.__class__ = type(...). Itu menyebabkan objek menjadi kelas yang berbeda dari yang Anda pikir Anda buat. Saya sebenarnya tidak bisa mengubahnya menjadi intseperti yang Anda lakukan ... Saya mendapatkan kesalahan tentang tipe tumpukan atau sesuatu ... tapi contoh saya menugaskannya ke karya kelas yang dibuat secara dinamis.
ArtOfWarfare
Saya juga bingung tentang kapan __init__()dipanggil. Misalnya, dalam jawaban lonetwin , baik Triangle.__init__()atau Square.__init__()secara otomatis dipanggil tergantung pada jenis yang __new__()dikembalikan. Dari apa yang Anda katakan dalam jawaban Anda (dan saya sudah membaca ini di tempat lain), sepertinya tidak satu pun dari keduanya karena Shape.__new__() tidak mengembalikan instance dari cls(atau salah satu dari subkelasnya).
martineau
1
@martineau: __init__()Metode dalam jawaban lonetwin dipanggil saat masing-masing objek dipakai (yaitu ketika metode mereka __new__() kembali), bukan ketika Shape.__new__()kembali.
Ignacio Vazquez-Abrams
Ahh, benar, Shape.__init__()(jika ada) tidak akan dipanggil. Sekarang semuanya lebih masuk akal ...:¬)
martineau
13

Bukan jawaban yang lengkap tapi mungkin sesuatu yang menggambarkan perbedaannya.

__new__akan selalu dipanggil ketika suatu objek harus dibuat. Ada beberapa situasi di mana __init__tidak akan dipanggil. Salah satu contoh adalah ketika Anda menghapus objek dari file acar, mereka akan dialokasikan ( __new__) tetapi tidak diinisialisasi ( __init__).

Noufal Ibrahim
sumber
Apakah saya akan memanggil init dari yang baru jika saya ingin memori dialokasikan dan data diinisialisasi? Mengapa jika baru tidak ada saat membuat instance init dipanggil?
redpix_
2
Tugas __new__metode ini adalah membuat (ini menyiratkan alokasi memori) sebuah instance dari kelas dan mengembalikannya. Inisialisasi adalah langkah terpisah dan itulah yang biasanya terlihat oleh pengguna. Silakan ajukan pertanyaan terpisah jika ada masalah khusus yang Anda hadapi.
Noufal Ibrahim
3

Hanya ingin menambahkan kata tentang maksud (sebagai lawan dari perilaku) mendefinisikan __new__versus__init__ .

Saya menemukan pertanyaan ini (antara lain) ketika saya mencoba memahami cara terbaik untuk mendefinisikan pabrik kelas. Saya menyadari bahwa salah satu cara yang __new__secara konseptual berbeda dari __init__adalah kenyataan bahwa manfaat __new__persis seperti yang dinyatakan dalam pertanyaan:

Jadi satu-satunya manfaat dari metode __new__ adalah bahwa variabel instance akan mulai sebagai string kosong, yang bertentangan dengan NULL. Tetapi mengapa ini pernah berguna, karena jika kita peduli untuk memastikan variabel instan kita diinisialisasi ke beberapa nilai default, kita bisa saja melakukannya dalam metode __init__?

Mempertimbangkan skenario yang dinyatakan, kami peduli tentang nilai-nilai awal dari variabel instan ketika instance sebenarnya adalah kelas itu sendiri. Jadi, jika kita secara dinamis membuat objek kelas saat runtime dan kita perlu mendefinisikan / mengendalikan sesuatu yang istimewa tentang instance kelas ini yang sedang dibuat, kita akan mendefinisikan kondisi / properti ini dalam __new__metode metaclass.

Saya bingung tentang hal ini sampai saya benar-benar memikirkan tentang penerapan konsep dan bukan hanya maknanya. Berikut ini contoh yang diharapkan akan membuat perbedaan menjadi jelas:

a = Shape(sides=3, base=2, height=12)
b = Shape(sides=4, length=2)
print(a.area())
print(b.area())

# I want `a` and `b` to be an instances of either of 'Square' or 'Triangle'
# depending on number of sides and also the `.area()` method to do the right
# thing. How do I do that without creating a Shape class with all the
# methods having a bunch of `if`s ? Here is one possibility

class Shape:
    def __new__(cls, sides, *args, **kwargs):
        if sides == 3:
            return Triangle(*args, **kwargs)
        else:
            return Square(*args, **kwargs)

class Triangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return (self.base * self.height) / 2

class Square:
    def __init__(self, length):
        self.length = length

    def area(self):
        return self.length*self.length

Perhatikan ini hanyalah contoh demonstratif. Ada beberapa cara untuk mendapatkan solusi tanpa menggunakan pendekatan pabrik kelas seperti di atas dan bahkan jika kita memang memilih untuk mengimplementasikan solusi dengan cara ini, ada sedikit peringatan yang ditinggalkan demi singkatnya (misalnya, mendeklarasikan metaclass secara eksplisit )

Jika Anda membuat kelas reguler (alias non-metaclass), maka __new__tidak terlalu masuk akal kecuali itu adalah kasus khusus seperti skenario yang bisa berubah versus yang tidak dapat diubah dalam jawaban jawaban ncoghlan (yang pada dasarnya adalah contoh yang lebih spesifik dari konsep mendefinisikan nilai / properti awal dari kelas / jenis yang dibuat via __new__untuk kemudian diinisialisasi melalui __init__).

Lonetwin
sumber