Làm thế nào để tôi dọn sạch chính xác một đối tượng Python?


462
class Package:
    def __init__(self):
        self.files = []

    # ...

    def __del__(self):
        for file in self.files:
            os.unlink(file)

__del__(self)ở trên không thành công với ngoại lệ AttributionError. Tôi hiểu Python không đảm bảo sự tồn tại của "biến toàn cục" (dữ liệu thành viên trong ngữ cảnh này?) Khi __del__()được gọi. Nếu đó là trường hợp và đây là lý do của ngoại lệ, làm thế nào để tôi chắc chắn rằng đối tượng phá hủy đúng cách?


3
Đọc những gì bạn đã liên kết, các biến toàn cục sẽ biến mất dường như không áp dụng ở đây trừ khi bạn nói về khi chương trình đang thoát, trong thời gian đó tôi đoán theo những gì bạn đã liên kết thì có thể POSSIBLE đã biến mất. Mặt khác, tôi không nghĩ rằng nó áp dụng cho các biến thành viên trong phương thức __del __ ().
Kevin Anderson

3
Ngoại lệ được ném lâu trước khi chương trình của tôi thoát ra. Ngoại lệ AttributionError mà tôi nhận được là Python nói rằng nó không nhận ra self.files là một thuộc tính của Gói. Tôi có thể đang hiểu sai điều này, nhưng nếu theo "toàn cầu" thì chúng không có nghĩa là biến toàn cục đối với các phương thức (nhưng có thể là cục bộ đối với lớp) thì tôi không biết điều gì gây ra ngoại lệ này. Google gợi ý Python có quyền dọn sạch dữ liệu thành viên trước khi __del __ (tự) được gọi.
wilmustell

1
Mã như được đăng dường như hoạt động với tôi (với Python 2.5). Bạn có thể đăng mã thực tế bị lỗi - hoặc đơn giản hóa (phiên bản đơn giản hơn mà vẫn gây ra lỗi không?
Silverfish

@ wilmustell bạn có thể cho một ví dụ cụ thể hơn? Trong tất cả các thử nghiệm của tôi, bộ hủy del hoạt động hoàn hảo.
Không biết

7
Nếu ai muốn biết: Bài viết này giải thích tại sao __del__không nên được sử dụng làm đối tác của __init__. (Tức là, nó không phải là "kẻ hủy diệt" theo nghĩa __init__là một nhà xây dựng.
franklin

Câu trả lời:


619

Tôi khuyên bạn nên sử dụng withcâu lệnh của Python để quản lý các tài nguyên cần được dọn sạch. Vấn đề với việc sử dụng một close()tuyên bố rõ ràng là bạn phải lo lắng về việc mọi người quên gọi nó hoặc quên đặt nó trong một finallykhối để ngăn chặn rò rỉ tài nguyên khi xảy ra ngoại lệ.

Để sử dụng withcâu lệnh, hãy tạo một lớp với các phương thức sau:

  def __enter__(self)
  def __exit__(self, exc_type, exc_value, traceback)

Trong ví dụ của bạn ở trên, bạn sẽ sử dụng

class Package:
    def __init__(self):
        self.files = []

    def __enter__(self):
        return self

    # ...

    def __exit__(self, exc_type, exc_value, traceback):
        for file in self.files:
            os.unlink(file)

Sau đó, khi ai đó muốn sử dụng lớp của bạn, họ sẽ làm như sau:

with Package() as package_obj:
    # use package_obj

Biến gói_obj sẽ là một thể hiện của kiểu Gói (đó là giá trị được __enter__phương thức trả về ). __exit__Phương thức của nó sẽ tự động được gọi, bất kể có xảy ra ngoại lệ hay không.

Bạn thậm chí có thể đưa phương pháp này một bước xa hơn. Trong ví dụ trên, ai đó vẫn có thể khởi tạo Gói bằng cách sử dụng hàm tạo của nó mà không cần sử dụng withmệnh đề. Bạn không muốn điều đó xảy ra. Bạn có thể khắc phục điều này bằng cách tạo một lớp PackageResource xác định các phương thức __enter____exit__. Sau đó, lớp Gói sẽ được định nghĩa đúng trong __enter__phương thức và được trả về. Theo cách đó, người gọi không bao giờ có thể khởi tạo lớp Gói mà không sử dụng withcâu lệnh:

class PackageResource:
    def __enter__(self):
        class Package:
            ...
        self.package_obj = Package()
        return self.package_obj

    def __exit__(self, exc_type, exc_value, traceback):
        self.package_obj.cleanup()

Bạn sẽ sử dụng như sau:

with PackageResource() as package_obj:
    # use package_obj

35
Về mặt kỹ thuật, người ta có thể gọi GóiResource () .__ nhập __ () một cách rõ ràng và do đó tạo ra Gói không bao giờ được hoàn thành ... nhưng họ thực sự phải cố gắng phá mã. Có lẽ không có gì phải lo lắng.
David Z

3
Nhân tiện, nếu bạn đang sử dụng Python 2.5, bạn sẽ cần thực hiện từ lần nhập tương lai với_statement để có thể sử dụng câu lệnh with.
Clint Miller

2
Tôi đã tìm thấy một bài viết giúp chỉ ra lý do tại sao __del __ () hành động theo cách đó và tin tưởng vào việc sử dụng giải pháp quản lý bối cảnh: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega

2
Làm thế nào để sử dụng cấu trúc đẹp và sạch đó nếu bạn muốn truyền tham số? Tôi muốn có thể làm đượcwith Resource(param1, param2) as r: # ...
snooze92

4
@ snooze92 bạn có thể cung cấp cho Tài nguyên một phương thức __init__ lưu trữ * args và ** kwargs trong bản thân, sau đó chuyển chúng vào lớp bên trong trong phương thức enter. Khi sử dụng câu lệnh with, __init__ được gọi trước __enter__
Brian Schlenker

48

Cách tiêu chuẩn là sử dụng atexit.register:

# package.py
import atexit
import os

class Package:
    def __init__(self):
        self.files = []
        atexit.register(self.cleanup)

    def cleanup(self):
        print("Running cleanup...")
        for file in self.files:
            print("Unlinking file: {}".format(file))
            # os.unlink(file)

Nhưng bạn nên nhớ rằng điều này sẽ tồn tại tất cả các phiên bản được tạo Packagecho đến khi Python bị chấm dứt.

Bản trình diễn sử dụng mã ở trên được lưu dưới dạng pack.py :

$ python
>>> from package import *
>>> p = Package()
>>> q = Package()
>>> q.files = ['a', 'b', 'c']
>>> quit()
Running cleanup...
Unlinking file: a
Unlinking file: b
Unlinking file: c
Running cleanup...

2
Điều hay ho về cách tiếp cận atexit.register là bạn không phải lo lắng về việc người dùng của lớp làm gì (họ đã sử dụng withchưa? Họ có gọi rõ ràng __enter__không?) Nhược điểm là tất nhiên nếu bạn cần dọn dẹp trước khi xảy ra trăn thoát ra, nó sẽ không hoạt động. Trong trường hợp của tôi, tôi không quan tâm nếu đó là khi đối tượng đi ra khỏi phạm vi hoặc nếu nó không cho đến khi con trăn thoát ra. :)
hlongmore

Tôi có thể sử dụng nhập và thoát và cũng có thể thêm atexit.register(self.__exit__)?
myradio

@myradio Tôi không thấy nó sẽ hữu ích như thế nào? Bạn không thể thực hiện tất cả logic dọn dẹp bên trong __exit__và sử dụng một bối cảnh? Ngoài ra, __exit__có các đối số bổ sung (ví dụ __exit__(self, type, value, traceback)), vì vậy bạn sẽ cần phải truy cập cho các đối số đó. Dù bằng cách nào, có vẻ như bạn nên đăng một câu hỏi riêng trên SO, vì trường hợp sử dụng của bạn có vẻ bất thường?
Ostrokach

33

Là phụ lục cho câu trả lời của Clint , bạn có thể đơn giản hóa PackageResourcebằng cách sử dụng contextlib.contextmanager:

@contextlib.contextmanager
def packageResource():
    class Package:
        ...
    package = Package()
    yield package
    package.cleanup()

Ngoài ra, mặc dù có thể không phải là Pythonic, bạn có thể ghi đè Package.__new__:

class Package(object):
    def __new__(cls, *args, **kwargs):
        @contextlib.contextmanager
        def packageResource():
            # adapt arguments if superclass takes some!
            package = super(Package, cls).__new__(cls)
            package.__init__(*args, **kwargs)
            yield package
            package.cleanup()

    def __init__(self, *args, **kwargs):
        ...

và chỉ cần sử dụng with Package(...) as package.

Để rút ngắn thời gian, hãy đặt tên cho chức năng dọn dẹp của bạn closevà sử dụng contextlib.closing, trong trường hợp đó bạn có thể sử dụng Packagelớp chưa sửa đổi thông qua with contextlib.closing(Package(...))hoặc ghi đè lên __new__hàm đơn giản hơn

class Package(object):
    def __new__(cls, *args, **kwargs):
        package = super(Package, cls).__new__(cls)
        package.__init__(*args, **kwargs)
        return contextlib.closing(package)

Và hàm tạo này được kế thừa, vì vậy bạn có thể chỉ cần kế thừa, vd

class SubPackage(Package):
    def close(self):
        pass

1
Điều này thật tuyệt. Tôi đặc biệt thích ví dụ cuối cùng. Tuy nhiên, thật không may là chúng ta không thể tránh được bản tóm tắt bốn dòng của Package.__new__()phương thức. Hoặc có lẽ chúng ta có thể. Chúng tôi có lẽ có thể định nghĩa một trình trang trí lớp hoặc siêu dữ liệu khái quát hóa cái nồi hơi đó cho chúng tôi. Thức ăn cho tư tưởng Pythonic.
Cecil Curry

@CecilCurry Cảm ơn, và điểm tốt. Bất kỳ lớp nào kế thừa Packagecũng nên làm điều này (mặc dù tôi chưa kiểm tra điều đó), vì vậy không cần phải có siêu dữ liệu. Mặc dù tôi đã tìm thấy một số cách khá tò mò để sử dụng siêu dữ liệu trong quá khứ ...
Tobias Kienzler

@CecilCurry Trên thực tế, hàm tạo được kế thừa, vì vậy bạn có thể sử dụng Package(hoặc tốt hơn là một lớp có tên Closing) làm lớp cha của bạn thay vì object. Nhưng đừng hỏi tôi làm thế nào nhiều di sản gây rối với điều này ...
Tobias Kienzler

17

Tôi không nghĩ rằng các thành viên thể hiện có thể bị xóa trước khi __del__được gọi. Tôi đoán là lý do cho AttributionError cụ thể của bạn là ở một nơi khác (có thể bạn đã xóa nhầm self.file ở nơi khác).

Tuy nhiên, như những người khác đã chỉ ra, bạn nên tránh sử dụng __del__. Lý do chính cho điều này là các trường hợp với __del__sẽ không được thu gom rác (chúng sẽ chỉ được giải phóng khi tổng số của chúng đạt 0). Do đó, nếu các phiên bản của bạn có liên quan đến các tham chiếu vòng tròn, chúng sẽ sống trong bộ nhớ miễn là ứng dụng chạy. (Tôi có thể bị nhầm lẫn về tất cả những điều này mặc dù, tôi phải đọc lại tài liệu gc, nhưng tôi chắc chắn rằng nó hoạt động như thế này).


5
Các đối tượng có __del__thể được thu gom rác nếu số tham chiếu của chúng từ các đối tượng khác __del__bằng 0 và chúng không thể truy cập được. Điều này có nghĩa là nếu bạn có một chu trình tham chiếu giữa các đối tượng với __del__, không có cái nào trong số chúng sẽ được thu thập. Bất kỳ trường hợp nào khác, tuy nhiên, nên được giải quyết như mong đợi.
Collin

"Bắt đầu với Python 3.4, các phương thức __del __ () không còn ngăn các chu trình tham chiếu bị thu gom rác và các mô đun toàn cầu không còn bị ép buộc trong khi tắt trình thông dịch. Vì vậy, mã này sẽ hoạt động mà không gặp vấn đề gì với CPython." - docs.python.org/3.6/library/...
Tomasz Gandor

14

Một cách khác tốt hơn là sử dụng yếu tố ref.finalize . Xem các ví dụ tại Finalizer ObjectSo sánh các công cụ hoàn thiện với các phương thức __del __ () .


1
Được sử dụng ngày hôm nay và nó hoạt động hoàn hảo, tốt hơn so với các giải pháp khác. Tôi có lớp giao tiếp dựa trên đa xử lý mở một cổng nối tiếp và sau đó tôi có một stop()phương thức để đóng các cổng và join()các quy trình. Tuy nhiên, nếu các chương trình thoát bất ngờ stop()không được gọi - tôi đã giải quyết điều đó bằng một bộ hoàn thiện. Nhưng trong mọi trường hợp, tôi gọi _finalizer.detach()phương thức dừng để ngăn chặn việc gọi nó hai lần (bằng tay và sau đó một lần nữa bởi bộ hoàn thiện).
Bojan P.

3
IMO, đây thực sự là câu trả lời tốt nhất. Nó kết hợp khả năng dọn dẹp tại bộ sưu tập rác với khả năng dọn sạch ở lối ra. Thông báo trước là python 2.7 không có yếu tố ref.finalize.
hlongmore

12

Tôi nghĩ vấn đề có thể xảy ra __init__nếu có nhiều mã hơn hiển thị?

__del__sẽ được gọi ngay cả khi __init__chưa được thực thi đúng hoặc ném ngoại lệ.

Nguồn


2
Âm thanh rất có thể. Cách tốt nhất để tránh vấn đề này khi sử dụng __del__là tuyên bố rõ ràng tất cả các thành viên ở cấp lớp, đảm bảo rằng chúng luôn tồn tại, ngay cả khi __init__thất bại. Trong ví dụ đã cho, files = ()sẽ hoạt động, mặc dù hầu hết bạn chỉ cần gán None; trong cả hai trường hợp, bạn vẫn cần gán giá trị thực trong __init__.
Søren Løvborg

11

Đây là một bộ xương làm việc tối thiểu:

class SkeletonFixture:

    def __init__(self):
        pass

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        pass

    def method(self):
        pass


with SkeletonFixture() as fixture:
    fixture.method()

Quan trọng: tự trả về


Nếu bạn giống tôi và bỏ qua return selfphần (trong câu trả lời đúng của Clint Miller ), bạn sẽ nhìn chằm chằm vào điều vô nghĩa này:

Traceback (most recent call last):
  File "tests/simplestpossible.py", line 17, in <module>                                                                                                                                                          
    fixture.method()                                                                                                                                                                                              
AttributeError: 'NoneType' object has no attribute 'method'

Hy vọng nó sẽ giúp người tiếp theo.


8

Chỉ cần bọc hàm hủy của bạn bằng câu lệnh try / trừ và nó sẽ không ném ngoại lệ nếu toàn cục của bạn đã được xử lý.

Biên tập

Thử cái này:

from weakref import proxy

class MyList(list): pass

class Package:
    def __init__(self):
        self.__del__.im_func.files = MyList([1,2,3,4])
        self.files = proxy(self.__del__.im_func.files)

    def __del__(self):
        print self.__del__.im_func.files

Nó sẽ nhồi danh sách tập tin vào chức năng del được đảm bảo tồn tại tại thời điểm gọi. Proxy yếu là để ngăn Python hoặc chính bạn xóa biến self.files bằng cách nào đó (nếu nó bị xóa, thì nó sẽ không ảnh hưởng đến danh sách tệp gốc). Nếu đây không phải là trường hợp bị xóa ngay cả khi có nhiều tham chiếu đến biến, thì bạn có thể loại bỏ đóng gói proxy.


2
Vấn đề là nếu dữ liệu thành viên biến mất thì quá muộn đối với tôi. Tôi cần dữ liệu đó. Xem mã của tôi ở trên: Tôi cần tên tệp để biết tệp nào cần xóa. Tôi đã đơn giản hóa mã của mình, tuy nhiên, có những dữ liệu khác tôi cần tự dọn dẹp (tức là người phiên dịch sẽ không biết cách làm sạch).
wilmustell

4

Có vẻ như cách thành ngữ để làm điều này là cung cấp một close()phương thức (hoặc tương tự), và gọi nó một cách rõ ràng.


20
Đây là cách tiếp cận tôi đã sử dụng trước đây, nhưng tôi gặp phải các vấn đề khác với nó. Với các ngoại lệ được các thư viện khác ném khắp nơi, tôi cần sự giúp đỡ của Python trong việc dọn dẹp mớ hỗn độn trong trường hợp có lỗi. Cụ thể, tôi cần Python gọi hàm hủy cho tôi, vì nếu không, mã sẽ nhanh chóng không thể quản lý được và tôi chắc chắn sẽ quên một điểm thoát nơi có lệnh gọi đến .close ().
wilmustell
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.