Python (và Python C API): __new__ so với __init__


126

Câu hỏi tôi sắp hỏi dường như là một bản sao của việc sử dụng __new__ và __init__ của Python? , nhưng bất kể, nó vẫn chưa rõ ràng với tôi chính xác sự khác biệt thực tế giữa __new____init__là gì.

Trước khi bạn vội vàng nói với tôi rằng đó __new__là để tạo ra các đối tượng và __init__là để khởi tạo các đối tượng, hãy để tôi nói rõ: tôi hiểu điều đó. Trên thực tế, sự khác biệt đó là khá tự nhiên đối với tôi, vì tôi có kinh nghiệm về C ++ nơi chúng tôi có vị trí mới , điều này tương tự tách biệt việc phân bổ đối tượng khỏi khởi tạo.

Các Python C API hướng dẫn giải thích nó như thế này:

Thành viên mới chịu trách nhiệm tạo các đối tượng (trái ngược với khởi tạo) của loại. Nó được hiển thị trong Python như là __new__()phương thức. ... Một lý do để thực hiện một phương thức mới là để đảm bảo các giá trị ban đầu của các biến thể hiện .

Vì vậy, yeah - tôi hiểu những gì __new__nó làm, nhưng mặc dù vậy, tôi vẫn không hiểu tại sao nó hữu ích trong Python. Ví dụ đưa ra nói rằng __new__có thể hữu ích nếu bạn muốn "đảm bảo các giá trị ban đầu của các biến thể hiện". Chà, chính xác thì __init__nó sẽ làm gì?

Trong hướng dẫn API C, một ví dụ được hiển thị trong đó Loại mới (được gọi là "Noddy") được tạo và __new__chức năng của Loại được xác định. Kiểu Noddy chứa một thành viên chuỗi được gọi firstvà thành viên chuỗi này được khởi tạo thành một chuỗi trống như vậy:

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

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

    .....
}

Lưu ý rằng không có __new__phương thức được xác định ở đây, chúng ta sẽ phải sử dụng PyType_GenericNew, đơn giản là khởi tạo tất cả các thành viên biến đối tượng thành NULL. Vì vậy, lợi ích duy nhất của __new__phương thức là biến thể hiện sẽ bắt đầu dưới dạng một chuỗi rỗng, trái ngược với NULL. Nhưng tại sao điều này lại hữu ích, vì nếu chúng ta quan tâm đến việc đảm bảo các biến đối tượng của chúng ta được khởi tạo thành một giá trị mặc định, chúng ta có thể thực hiện điều đó trong __init__phương thức không?

Câu trả lời:


137

Sự khác biệt chủ yếu phát sinh với các loại đột biến và bất biến.

__new__chấp nhận một loại làm đối số đầu tiên và (thường) trả về một thể hiện mới của loại đó. Do đó, nó phù hợp để sử dụng với cả hai loại đột biến và bất biến.

__init__chấp nhận một thể hiện làm đối số đầu tiên và sửa đổi các thuộc tính của thể hiện đó. Điều này không phù hợp với loại không thay đổi, vì nó sẽ cho phép chúng được sửa đổi sau khi tạo bằng cách gọi obj.__init__(*args).

So sánh hành vi của tuplelist:

>>> 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]

Về lý do tại sao chúng tách biệt (ngoài các lý do lịch sử đơn giản): __new__các phương thức yêu cầu một bó nồi hơi để có quyền (việc tạo đối tượng ban đầu, và sau đó ghi nhớ để trả lại đối tượng ở cuối). __init__ngược lại, các phương thức rất đơn giản, vì bạn chỉ cần đặt bất kỳ thuộc tính nào bạn cần đặt.

Ngoài __init__các phương thức dễ viết hơn và sự khác biệt có thể thay đổi so với bất biến được ghi chú ở trên, sự phân tách cũng có thể được khai thác để gọi lớp cha __init__trong các lớp con tùy chọn bằng cách thiết lập bất kỳ trường hợp bất biến hoàn toàn bắt buộc nào __new__. Đây thường là một cách thực hành đáng ngờ - thường chỉ cần gọi các __init__phương thức lớp cha là cần thiết.


1
mã mà bạn gọi là "mẫu soạn sẵn" __new__không phải là bản mẫu, vì bản mẫu không bao giờ thay đổi. Đôi khi bạn cần thay thế mã cụ thể đó bằng một cái gì đó khác nhau.
Miles Rout

13
Tạo, hoặc bằng cách khác có được, ví dụ (thường là với một supercuộc gọi) và trả lại cá thể là những phần cần thiết của bất kỳ __new__triển khai nào , và "cái nồi hơi" mà tôi đang đề cập đến. Ngược lại, passlà một triển khai hợp lệ cho __init__- không có hành vi bắt buộc nào.
ncoghlan

37

Có thể có những cách sử dụng khác cho __new__nhưng có một cách thực sự rõ ràng: Bạn không thể phân lớp một loại bất biến mà không sử dụng __new__. Vì vậy, ví dụ, giả sử bạn muốn tạo một lớp con của bộ dữ liệu chỉ có thể chứa các giá trị tích phân giữa 0 và 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)

Bạn chỉ đơn giản là không thể làm điều này với __init__- nếu bạn cố gắng sửa đổi selftrong __init__, người phiên dịch sẽ phàn nàn rằng bạn đang cố gắng sửa đổi một đối tượng không thay đổi.


1
Tôi không hiểu tại sao chúng ta nên sử dụng siêu? Ý tôi là tại sao mới phải trả về một thể hiện của siêu lớp? Hơn nữa, như bạn đã nói, tại sao chúng ta phải chuyển cls rõ ràng sang mới ? super (ModularTuple, cls) không trả về một phương thức ràng buộc?
Alcott

3
@ Alcott, tôi nghĩ bạn đang hiểu nhầm hành vi của __new__. Chúng tôi chuyển clsmột cách rõ ràng __new__bởi vì, như bạn có thể đọc ở đây __new__ luôn yêu cầu một kiểu làm đối số đầu tiên của nó. Sau đó nó trả về một thể hiện của kiểu đó. Vì vậy, chúng tôi sẽ không trả lại một thể hiện của siêu lớp - chúng tôi sẽ trả lại một thể hiện của cls. Trong trường hợp này, nó giống như chúng ta đã nói tuple.__new__(ModularTuple, tup).
gửi

35

__new__()có thể trả về các đối tượng thuộc loại khác ngoài lớp mà nó bị ràng buộc. __init__()chỉ khởi tạo một thể hiện hiện có của lớp.

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

Đây là lời giải thích gọn gàng nhất cho đến nay.
Tarik

Không hoàn toàn đúng. Tôi có __init__các phương thức chứa mã trông như thế self.__class__ = type(...). Điều đó làm cho đối tượng thuộc một lớp khác với đối tượng bạn nghĩ bạn đang tạo. Tôi thực sự không thể thay đổi nó thành intnhư bạn đã làm ... Tôi gặp lỗi về các loại heap hoặc thứ gì đó ... nhưng ví dụ của tôi về việc gán nó cho một lớp được tạo động sẽ hoạt động.
ArtOfWarfare

Tôi cũng vậy, tôi bối rối khi __init__()được gọi. Ví dụ: trong câu trả lời của lonetwin , Triangle.__init__()hoặc Square.__init__()tự động được gọi tùy thuộc vào loại __new__()trả về. Từ những gì bạn nói trong câu trả lời của bạn (và tôi đã đọc ở đâu đó này), nó không có vẻ như cả hai người nên kể từ khi Shape.__new__() không được trở về một thể hiện của cls(hay một trong một lớp con của nó).
martineau

1
@martineau: Các __init__()phương thức trong câu trả lời của lonetwin được gọi khi các đối tượng riêng lẻ được khởi tạo (tức là khi phương thức của chúng __new__() trả về), chứ không phải khi Shape.__new__()trả về.
Ignacio Vazquez-Abrams

À, phải rồi, Shape.__init__()(nếu có) sẽ không được gọi. Bây giờ tất cả đều có ý nghĩa hơn ...:¬)
martineau

13

Không phải là một câu trả lời đầy đủ nhưng có lẽ một cái gì đó minh họa sự khác biệt.

__new__sẽ luôn được gọi khi một đối tượng phải được tạo. Có một số tình huống __init__sẽ không được gọi. Một ví dụ là khi bạn giải nén các đối tượng từ tệp dưa chua, chúng sẽ được cấp phát ( __new__) nhưng không được khởi tạo ( __init__).


Tôi có thể gọi init từ mới nếu tôi muốn bộ nhớ được phân bổ và dữ liệu sẽ được khởi tạo? Tại sao nếu mới không tồn tại khi tạo init init được gọi?
redpix_

2
Công việc của __new__phương thức là tạo (điều này hàm ý cấp phát bộ nhớ) một thể hiện của lớp và trả về nó. Việc khởi tạo là một bước riêng biệt và đó là những gì thường được hiển thị cho người dùng. Vui lòng đặt câu hỏi riêng nếu có vấn đề cụ thể mà bạn gặp phải.
Noufal Ibrahim

3

Chỉ muốn thêm một từ về ý định (trái ngược với hành vi) của việc xác định __new__so với __init__.

Tôi đã gặp câu hỏi này (trong số những người khác) khi tôi đang cố gắng hiểu cách tốt nhất để xác định một nhà máy sản xuất lớp. Tôi nhận ra rằng một trong những cách __new__khác biệt về mặt khái niệm __init__là thực tế rằng lợi ích của __new__chính xác là những gì đã được nêu trong câu hỏi:

Vì vậy, lợi ích duy nhất của phương thức __new__ là biến thể hiện sẽ bắt đầu dưới dạng một chuỗi rỗng, trái ngược với NULL. Nhưng tại sao điều này lại hữu ích, vì nếu chúng ta quan tâm đến việc đảm bảo các biến đối tượng của chúng ta được khởi tạo thành một giá trị mặc định nào đó, chúng ta có thể thực hiện điều đó trong phương thức __init__ không?

Xem xét kịch bản đã nêu, chúng tôi quan tâm đến các giá trị ban đầu của các biến thể hiện khi thực thể là một lớp. Vì vậy, nếu chúng ta tự động tạo một đối tượng lớp trong thời gian chạy và chúng ta cần định nghĩa / điều khiển một cái gì đó đặc biệt về các phiên bản tiếp theo của lớp này, chúng ta sẽ định nghĩa các điều kiện / thuộc tính này trong một __new__phương thức của siêu dữ liệu.

Tôi đã nhầm lẫn về điều này cho đến khi tôi thực sự nghĩ về việc áp dụng khái niệm chứ không chỉ là ý nghĩa của nó. Đây là một ví dụ hy vọng sẽ làm cho sự khác biệt rõ ràng:

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

Lưu ý đây chỉ là một ví dụ quỷ dị. Có nhiều cách để có được giải pháp mà không cần dùng đến cách tiếp cận của nhà máy lớp như trên và ngay cả khi chúng tôi chọn áp dụng giải pháp theo cách này, vẫn có một chút cảnh giác vì lý do ngắn gọn (ví dụ, tuyên bố siêu dữ liệu một cách rõ ràng )

Nếu bạn đang tạo một lớp thông thường (còn gọi là không phải siêu dữ liệu), thì __new__thực sự không có ý nghĩa gì trừ khi đó là trường hợp đặc biệt như kịch bản có thể thay đổi so với bất biến trong câu trả lời của ncoghlan (về cơ bản là một ví dụ cụ thể hơn về khái niệm định nghĩa các giá trị / thuộc tính ban đầu của lớp / loại được tạo thông qua __new__để sau đó được khởi tạo thông qua __init__).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.