Python (และ Python C API): __new__ เทียบกับ __init__


127

คำถามที่ฉันกำลังจะถามดูเหมือนจะซ้ำกันกับการใช้ __new__ และ __init__ของPython? แต่อย่างไรก็ตามก็ยังไม่ชัดเจนสำหรับฉันว่าความแตกต่างในทางปฏิบัติระหว่าง__new__และ__init__คืออะไร

ก่อนที่คุณจะรีบบอกฉันว่า__new__มีไว้สำหรับสร้างวัตถุและ__init__มีไว้สำหรับการเริ่มต้นอ็อบเจ็กต์ขอฉันพูดให้ชัดเจน: ฉันเข้าใจ ในความเป็นจริงความแตกต่างนั้นค่อนข้างเป็นธรรมชาติสำหรับฉันเนื่องจากฉันมีประสบการณ์ใน C ++ ที่เรามีการจัดวางใหม่ซึ่งแยกการจัดสรรอ็อบเจ็กต์ออกจากการเริ่มต้นในทำนองเดียวกัน

หลาม C API กวดวิชาอธิบายเช่นนี้

สมาชิกใหม่มีหน้าที่สร้างอ็อบเจ็กต์ประเภท (ซึ่งตรงข้ามกับการกำหนดค่าเริ่มต้น) มันถูกเปิดเผยใน Python เป็น__new__()วิธีการ ... เหตุผลหนึ่งที่จะใช้วิธีการใหม่เพื่อให้มั่นใจค่าเริ่มต้นของตัวแปรเช่น

ใช่แล้ว - ฉันได้อะไร__new__แต่ถึงอย่างนั้นฉันก็ยังไม่เข้าใจว่าทำไมมันถึงมีประโยชน์ใน Python ตัวอย่างที่ระบุบอกว่า__new__อาจมีประโยชน์หากคุณต้องการ "รับรองค่าเริ่มต้นของตัวแปรอินสแตนซ์" ก็ใช่ว่า__init__จะทำอะไร?

ในบทช่วยสอน C API ตัวอย่างจะแสดงที่สร้าง Type ใหม่ (เรียกว่า "Noddy") และกำหนด__new__ฟังก์ชันของ Type ประเภท Noddy ประกอบด้วยสมาชิกสตริงที่เรียกว่าfirstและสมาชิกสตริงนี้เริ่มต้นเป็นสตริงว่างดังนี้:

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

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

    .....
}

โปรดทราบว่าหากไม่มี__new__วิธีการที่กำหนดไว้ที่นี่เราจะต้องใช้PyType_GenericNewซึ่งเพียงแค่เริ่มต้นสมาชิกตัวแปรอินสแตนซ์ทั้งหมดเป็น NULL ดังนั้นประโยชน์เพียงอย่างเดียวของ__new__วิธีนี้คือตัวแปรอินสแตนซ์จะเริ่มต้นเป็นสตริงว่างซึ่งตรงข้ามกับ NULL แต่เหตุใดสิ่งนี้จึงมีประโยชน์เนื่องจากหากเราใส่ใจในการตรวจสอบให้แน่ใจว่าตัวแปรอินสแตนซ์ของเราเริ่มต้นเป็นค่าเริ่มต้นบางอย่างเราสามารถทำได้ใน__init__วิธีนี้

คำตอบ:


137

ความแตกต่างส่วนใหญ่เกิดขึ้นกับประเภทที่ผันแปรและไม่เปลี่ยนรูป

__new__ยอมรับประเภทเป็นอาร์กิวเมนต์แรกและ (โดยปกติ) จะส่งคืนอินสแตนซ์ใหม่ของประเภทนั้น ดังนั้นจึงเหมาะสำหรับการใช้งานทั้งประเภทที่แปรเปลี่ยนและไม่เปลี่ยนรูป

__init__ยอมรับอินสแตนซ์เป็นอาร์กิวเมนต์แรกและแก้ไขแอตทริบิวต์ของอินสแตนซ์นั้น obj.__init__(*args)นี้ไม่เหมาะสมกับชนิดไม่เปลี่ยนรูปมันจะช่วยให้พวกเขาได้รับการแก้ไขหลังจากการสร้างโดยการเรียก

เปรียบเทียบพฤติกรรมของtupleและ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]

สาเหตุที่พวกมันแยกจากกัน (นอกเหนือจากเหตุผลทางประวัติศาสตร์ง่ายๆ): __new__วิธีการต้องใช้แผ่นสำเร็จรูปจำนวนมากเพื่อให้ถูกต้อง (การสร้างวัตถุเริ่มต้นจากนั้นอย่าลืมส่งคืนวัตถุในตอนท้าย) __init__ในทางตรงกันข้ามวิธีการนั้นง่ายมากเนื่องจากคุณเพียงแค่ตั้งค่าคุณลักษณะที่คุณต้องการตั้งค่า

นอกเหนือจาก__init__วิธีการที่ง่ายต่อการเขียนและความแตกต่างที่เปลี่ยนแปลงได้และไม่เปลี่ยนรูปที่ระบุไว้ข้างต้นแล้วการแยกยังสามารถใช้ประโยชน์เพื่อทำให้การเรียกคลาสพาเรนต์__init__ในคลาสย่อยเป็นทางเลือกโดยการตั้งค่าอินสแตนซ์อินสแตนซ์ที่จำเป็นอย่างยิ่งใน__new__. โดยทั่วไปแล้วนี่เป็นวิธีปฏิบัติที่น่าสงสัย - โดยปกติจะชัดเจนกว่าเพียงแค่เรียก__init__เมธอดคลาสผู้ปกครองตามความจำเป็น


1
รหัสที่คุณเรียกว่า "ต้นแบบ" ใน__new__ไม่ใช่สำเร็จรูปเนื่องจากต้นแบบไม่เคยเปลี่ยนแปลง บางครั้งคุณต้องแทนที่รหัสนั้นด้วยสิ่งที่แตกต่างออกไป
เส้นทางไมล์

13
การสร้างหรือการรับอินสแตนซ์ (โดยปกติจะมีการsuperโทร) และการส่งคืนอินสแตนซ์เป็นส่วนที่จำเป็นของ__new__การนำไปใช้งานใด ๆและ "ต้นแบบ" ที่ฉันอ้างถึง ในทางตรงกันข้ามpassเป็นการใช้งานที่ถูกต้องสำหรับ__init__- ไม่มีพฤติกรรมที่จำเป็นใด ๆ
ncoghlan

38

อาจจะมีการใช้อื่น ๆ สำหรับการเป็นคุณไม่สามารถซับคลาสชนิดไม่เปลี่ยนรูปโดยไม่ต้องใช้__new__ แต่มีหนึ่งที่เห็นได้ชัดจริงๆหนึ่ง __new__ดังนั้นสำหรับตัวอย่างเช่นสมมติว่าคุณต้องการที่จะสร้าง subclass ของ tuple ที่สามารถมีค่าเพียงหนึ่งระหว่าง 0 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)

คุณก็ไม่สามารถทำเช่นนี้กับ__init__- ถ้าคุณพยายามที่จะปรับเปลี่ยนselfใน__init__ล่ามจะบ่นว่าคุณกำลังพยายามที่จะปรับเปลี่ยนวัตถุไม่เปลี่ยนรูป


1
ไม่เข้าใจว่าทำไมต้องใช้ super? ผมหมายถึงว่าทำไมควรใหม่กลับตัวอย่างของ superclass ๆ ? นอกจากนี้ตามที่คุณวางไว้ทำไมเราต้องส่ง cls ไปยังnewอย่างชัดเจน? super (ModularTuple, cls) ไม่ส่งคืนวิธีการผูก?
Alcott

3
@Alcott ฉันคิดว่าคุณเข้าใจพฤติกรรมของ__new__. เราผ่านclsอย่างชัดเจน__new__เพราะเป็นคุณสามารถอ่านที่นี่ __new__ มักจะต้องมีชนิดเป็นอาร์กิวเมนต์แรกของมัน จากนั้นจะส่งคืนอินสแตนซ์ของประเภทนั้น ดังนั้นเราจะไม่กลับมาเป็นตัวอย่างของ superclass ๆ - clsเรากำลังกลับมาอินสแตนซ์ของ ในกรณีนี้ก็เหมือนกับที่เราพูดtuple.__new__(ModularTuple, tup)ไป
ส่ง

35

__new__()สามารถส่งคืนอ็อบเจ็กต์ประเภทอื่นที่ไม่ใช่คลาสที่ถูกผูกไว้ __init__()เริ่มต้นอินสแตนซ์ที่มีอยู่ของคลาสเท่านั้น

>>> class C(object):
...   def __new__(cls):
...     return 5
...
>>> c = C()
>>> print type(c)
<type 'int'>
>>> print c
5

นี่เป็นคำอธิบายที่น้อยที่สุดจนถึงตอนนี้
Tarik

ไม่เป็นความจริง ผมมีวิธีการที่มีรหัสที่ดูเหมือนว่า__init__ self.__class__ = type(...)นั่นทำให้ออบเจ็กต์เป็นคลาสที่แตกต่างจากคลาสที่คุณคิดว่าคุณสร้างขึ้น ฉันไม่สามารถเปลี่ยนเป็นไฟล์intที่คุณทำได้จริงๆ ... ฉันได้รับข้อผิดพลาดเกี่ยวกับประเภทฮีปหรือบางสิ่ง ... แต่ตัวอย่างของฉันในการกำหนดให้กับคลาสที่สร้างแบบไดนามิกทำงานได้
ArtOfWarfare

ฉันเองก็สับสนเหมือนกันว่าเมื่อไหร่ที่__init__()ถูกเรียก ยกตัวอย่างเช่นในคำตอบของ lonetwinอย่างใดอย่างหนึ่งTriangle.__init__()หรือSquare.__init__()โดยอัตโนมัติได้รับการเรียกขึ้นอยู่กับประเภท__new__()ผลตอบแทน จากสิ่งที่คุณพูดในคำตอบของคุณ (และฉันได้อ่านที่อื่น) ดูเหมือนว่าไม่ควรเป็นอย่างใดอย่างหนึ่งเนื่องจากShape.__new__() ไม่ใช่ส่งคืนอินสแตนซ์ของcls(หรือคลาสย่อยของคลาสใดคลาสหนึ่ง)
martineau

1
@martineau: __init__()วิธีการในคำตอบของ lonetwin ถูกเรียกใช้เมื่อวัตถุแต่ละชิ้นได้รับการสร้างอินสแตนซ์ (เช่นเมื่อวิธีการของพวกเขา __new__()ส่งคืน) ไม่ใช่เมื่อShape.__new__()ส่งคืน
Ignacio Vazquez-Abrams

อ่าใช่Shape.__init__()(ถ้ามี) จะไม่เรียกว่า ตอนนี้ทุกอย่างสมเหตุสมผลมากขึ้น ...:¬)
martineau

13

ไม่ใช่คำตอบที่สมบูรณ์ แต่อาจเป็นสิ่งที่แสดงให้เห็นถึงความแตกต่าง

__new__จะถูกเรียกเสมอเมื่อต้องสร้างวัตถุ มีบางสถานการณ์ที่__init__จะไม่ถูกเรียก ตัวอย่างหนึ่งคือเมื่อคุณปลดวัตถุออกจากไฟล์ดองพวกมันจะได้รับการจัดสรร ( __new__) แต่ไม่ได้เริ่มต้น ( __init__)


ฉันจะเรียกใช้initจากnew หรือไม่ถ้าฉันต้องการให้มีการจัดสรรหน่วยความจำและข้อมูลเริ่มต้น เหตุใดหากไม่มีสิ่งใหม่เมื่อสร้างอินสแตนซ์init จึงถูกเรียก
redpix_

2
งานของ__new__เมธอดคือการสร้าง (ซึ่งหมายถึงการจัดสรรหน่วยความจำ) อินสแตนซ์ของคลาสและส่งคืน การเริ่มต้นเป็นขั้นตอนแยกต่างหากและเป็นสิ่งที่ผู้ใช้มักมองเห็นได้ โปรดถามคำถามแยกต่างหากหากมีปัญหาเฉพาะที่คุณกำลังเผชิญ
Noufal Ibrahim

3

เพียงแค่ต้องการเพิ่มคำเกี่ยวกับเจตนา (ตรงข้ามกับพฤติกรรม) ของการกำหนด__new__เทียบกับ__init__เมื่อเทียบกับ

ฉันเจอคำถามนี้ (ท่ามกลางคำถามอื่น ๆ ) เมื่อฉันพยายามทำความเข้าใจวิธีที่ดีที่สุดในการกำหนดโรงงานชั้นเรียน ฉันตระหนักว่าวิธีหนึ่งที่__new__แตกต่างจากแนวคิด__init__คือความจริงที่ว่าประโยชน์ของ__new__มันคือสิ่งที่ระบุไว้ในคำถาม:

ดังนั้นประโยชน์อย่างเดียวของเมธอด __new__ คือตัวแปรอินสแตนซ์จะเริ่มต้นเป็นสตริงว่างซึ่งตรงข้ามกับ NULL แต่เหตุใดสิ่งนี้จึงมีประโยชน์เนื่องจากหากเราใส่ใจในการตรวจสอบให้แน่ใจว่าตัวแปรอินสแตนซ์ของเราเริ่มต้นเป็นค่าเริ่มต้นเราสามารถทำได้ในเมธอด __init__

เมื่อพิจารณาถึงสถานการณ์ที่ระบุเราสนใจเกี่ยวกับค่าเริ่มต้นของตัวแปรอินสแตนซ์เมื่ออินสแตนซ์อยู่ในคลาสตัวเอง ดังนั้นหากเรากำลังสร้างคลาสอ็อบเจ็กต์แบบไดนามิกที่รันไทม์และเราจำเป็นต้องกำหนด / ควบคุมสิ่งที่พิเศษเกี่ยวกับอินสแตนซ์ที่ตามมาของคลาสนี้ที่สร้างขึ้นเราจะกำหนดเงื่อนไข / คุณสมบัติเหล่านี้ใน a__new__วิธีการของเมตาคลาส

ฉันสับสนเกี่ยวกับเรื่องนี้จนกระทั่งฉันคิดเกี่ยวกับการประยุกต์ใช้แนวคิดมากกว่าแค่ความหมายของมัน นี่คือตัวอย่างที่หวังว่าจะสร้างความแตกต่างให้ชัดเจน:

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

โปรดทราบว่านี่เป็นเพียงตัวอย่างที่แสดงให้เห็นเท่านั้น มีหลายวิธีในการหาวิธีแก้ปัญหาโดยไม่ต้องใช้วิธีการแบบโรงงานเช่นข้างต้นและแม้ว่าเราจะเลือกใช้วิธีการแก้ปัญหาในลักษณะนี้ แต่ก็มีข้อแม้เล็ก ๆ น้อย ๆ ที่ทิ้งไว้เพื่อความสั้น (ตัวอย่างเช่นการประกาศ metaclass อย่างชัดเจน )

หากคุณกำลังสร้างคลาสปกติ (aka non-metaclass) ก็__new__ไม่สมเหตุสมผลเว้นแต่จะเป็นกรณีพิเศษเช่นสถานการณ์ที่เปลี่ยนแปลงไม่ได้และไม่เปลี่ยนรูปในคำตอบของ ncoghlan (ซึ่งโดยพื้นฐานแล้วเป็นตัวอย่างที่เฉพาะเจาะจงมากขึ้นของแนวคิดในการกำหนด ค่าเริ่มต้น / คุณสมบัติของคลาส / ประเภทที่สร้างขึ้น__new__เพื่อเริ่มต้นจากนั้น__init__)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.