ฉันจะล้างวัตถุ Python อย่างถูกต้องได้อย่างไร


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

    # ...

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

__del__(self)ด้านบนล้มเหลวด้วยข้อยกเว้นของ AttributeError ฉันเข้าใจว่าPython ไม่รับประกันการมีอยู่ของ "ตัวแปรทั่วโลก" (ข้อมูลสมาชิกในบริบทนี้?) เมื่อ__del__()มีการเรียกใช้ หากเป็นเช่นนั้นและนี่คือเหตุผลสำหรับข้อยกเว้นฉันจะแน่ใจได้อย่างไรว่าวัตถุถูกทำลายอย่างถูกต้อง


3
อ่านสิ่งที่คุณเชื่อมโยงตัวแปรทั่วโลกที่หายไปดูเหมือนจะไม่ใช้ที่นี่เว้นแต่ว่าคุณกำลังพูดถึงเมื่อคุณออกจากโปรแกรมในระหว่างที่ฉันเดาตามสิ่งที่คุณเชื่อมโยงมันอาจเป็นไปได้ว่าโมดูล OS นั้นหายไปแล้ว มิฉะนั้นฉันไม่คิดว่ามันจะใช้กับตัวแปรสมาชิกในเมธอด __del __ ()
เควินแอนเดอ

3
ข้อยกเว้นเกิดขึ้นนานก่อนที่โปรแกรมของฉันจะออก ข้อยกเว้น AttributeError ที่ฉันได้รับคือ Python บอกว่าไม่ได้รู้จัก self.files ว่าเป็นแอตทริบิวต์ของ Package ฉันอาจจะผิดนี้ แต่ถ้าโดย "กลม" พวกเขาไม่ได้หมายถึงตัวแปรทั่วโลกเพื่อวิธีการ (แต่อาจจะเป็นในระดับท้องถิ่น) แล้วฉันไม่รู้ว่าสิ่งที่ทำให้เกิดข้อยกเว้นนี้ คำแนะนำของ Google Python ขอสงวนสิทธิ์ในการล้างข้อมูลสมาชิกก่อนที่จะเรียก __del __ (ตนเอง)
wilhelmtell

1
รหัสตามที่โพสต์ดูเหมือนจะใช้ได้กับฉัน (ด้วย Python 2.5) คุณสามารถโพสต์รหัสจริงว่าเป็นความล้มเหลว - หรือง่าย (ง่ายดีกว่ารุ่นที่ยังคงเกิดข้อผิดพลาดหรือไม่?
สีเงิน

@ wilhelmtell คุณช่วยยกตัวอย่างที่เป็นรูปธรรมมากขึ้นได้ไหม? ในการทดสอบทั้งหมดของฉันเดล destructor ทำงานได้อย่างสมบูรณ์แบบ
ไม่ทราบ

7
ถ้าใครอยากจะรู้: บทความนี้ elaborates เหตุผลที่ไม่ควรนำมาใช้เป็นคู่ของ__del__ __init__(คือมันไม่ได้เป็น "เตาเผา" ในแง่ที่ว่า__init__เป็นผู้สร้าง.
แฟรงคลิน

คำตอบ:


619

ฉันขอแนะนำให้ใช้withคำสั่งPython สำหรับการจัดการทรัพยากรที่จำเป็นต้องทำความสะอาด ปัญหาเกี่ยวกับการใช้close()คำสั่งที่ชัดเจนคือคุณต้องกังวลเกี่ยวกับคนที่ลืมโทรเลยหรือลืมวางไว้ในfinallyบล็อกเพื่อป้องกันการรั่วไหลของทรัพยากรเมื่อมีข้อยกเว้นเกิดขึ้น

ในการใช้withคำสั่งให้สร้างคลาสด้วยวิธีการต่อไปนี้:

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

ในตัวอย่างด้านบนคุณจะใช้

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)

จากนั้นเมื่อมีคนต้องการใช้คลาสของคุณพวกเขาจะทำสิ่งต่อไปนี้:

with Package() as package_obj:
    # use package_obj

ตัวแปร package_obj จะเป็นตัวอย่างของประเภทแพคเกจ (มันเป็นค่าที่ส่งกลับโดย__enter__วิธีการ) __exit__วิธีการของมันจะถูกเรียกโดยอัตโนมัติไม่ว่าจะมีข้อยกเว้นเกิดขึ้นหรือไม่

คุณสามารถใช้แนวทางนี้อีกก้าวหนึ่ง ในตัวอย่างข้างต้นบางคนอาจจะยังคง instantiate แพคเกจโดยใช้ตัวสร้างได้โดยไม่ต้องใช้withประโยค คุณไม่ต้องการที่จะเกิดขึ้น คุณสามารถแก้ไขได้โดยสร้างคลาส PackageResource ที่กำหนด__enter__และ__exit__วิธีการ จากนั้นคลาสแพ็กเกจจะถูกกำหนดอย่างเคร่งครัดภายใน__enter__เมธอดและส่งคืน ด้วยวิธีนี้ผู้เรียกจะไม่สามารถยกระดับคลาสแพ็คเกจได้โดยไม่ต้องใช้withคำสั่ง

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()

คุณจะใช้สิ่งนี้ดังนี้:

with PackageResource() as package_obj:
    # use package_obj

35
เทคนิคการพูดใคร ๆ ก็เรียก PackageResource () .__ ป้อน __ () อย่างชัดเจนและสร้างแพ็คเกจที่จะไม่มีวันสรุป ... แต่พวกเขาต้องพยายามทำลายรหัส อาจไม่ใช่สิ่งที่ต้องกังวล
David Z

3
อย่างไรก็ตามถ้าคุณใช้ Python 2.5 คุณจะต้องทำการนำเข้าในอนาคต with_statement เพื่อให้สามารถใช้คำสั่ง with
Clint Miller

2
ผมพบว่าบทความที่ช่วยในการแสดงให้เห็นว่าทำไม __del __ () ทำหน้าที่ทางมันไม่และให้ความเชื่อถือกับการใช้วิธีการแก้ปัญหาผู้จัดการบริบท: andy-pearce.com/blog/posts/2013/Apr/python-destructor-drawbacks
eikonomega

2
จะใช้โครงสร้างที่ดีและสะอาดได้อย่างไรหากคุณต้องการส่งพารามิเตอร์ ฉันต้องการที่จะสามารถทำwith Resource(param1, param2) as r: # ...
snooze92

4
@ snooze92 คุณสามารถให้ทรัพยากรวิธี __init__ ที่เก็บ * args และ ** kwargs ด้วยตนเองแล้วส่งต่อไปยังคลาสภายในในวิธีการป้อน เมื่อใช้คำสั่ง with with __init__ จะถูกเรียกก่อน __enter__
Brian Schlenker

48

วิธีมาตรฐานคือการใช้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)

แต่คุณควรจำไว้ว่าสิ่งนี้จะคงอยู่อินสแตนซ์ที่สร้างขึ้นทั้งหมดPackageจนกว่า Python จะถูกยกเลิก

สาธิตโดยใช้รหัสด้านบนบันทึกเป็นpackage.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
สิ่งที่ดีเกี่ยวกับวิธี atexit.register คือคุณไม่ต้องกังวลเกี่ยวกับสิ่งที่ผู้ใช้ของคลาสทำ (พวกเขาใช้withหรือไม่พวกเขาเรียกอย่างชัดเจน__enter__?) ข้อเสียคือแน่นอนถ้าคุณต้องการล้างข้อมูลให้เกิดขึ้นก่อนไพ ธ อน ทางออกมันจะไม่ทำงาน ในกรณีของฉันฉันไม่สนใจว่ามันเป็นเมื่อวัตถุออกจากขอบเขตหรือถ้ามันไม่ได้จนกว่างูหลามออก :)
hlongmore

ฉันสามารถใช้การเข้าและออกและเพิ่มได้atexit.register(self.__exit__)หรือไม่
myradio

@myradio ฉันไม่เห็นว่าจะมีประโยชน์อย่างไร คุณไม่สามารถดำเนินการตรรกะการล้างข้อมูลภายใน__exit__และใช้ตัวจัดการบริบทได้หรือไม่ นอกจากนี้__exit__รับข้อโต้แย้งเพิ่มเติม (เช่น__exit__(self, type, value, traceback)) ดังนั้นคุณจะต้องมีบัญชี ไม่ว่าจะด้วยวิธีใดดูเหมือนว่าคุณควรโพสต์คำถามแยกต่างหากใน SO เนื่องจากกรณีการใช้งานของคุณดูเหมือนผิดปกติ
ostrokach

33

ในฐานะที่เป็นภาคผนวกของคำตอบของ Clintคุณสามารถลดความซับซ้อนของการPackageResourceใช้contextlib.contextmanager:

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

อีกทางเลือกหนึ่งแม้ว่าอาจจะไม่ได้เป็น Pythonic คุณสามารถแทนที่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):
        ...

with Package(...) as packageและเพียงแค่ใช้

เพื่อให้สิ่งต่าง ๆ สั้นลงให้ตั้งชื่อฟังก์ชั่นการล้างข้อมูลcloseและการใช้งานของcontextlib.closingคุณซึ่งในกรณีนี้คุณสามารถใช้Packageคลาสที่ไม่มีการแก้ไขผ่านwith contextlib.closing(Package(...))หรือแทนที่มัน__new__เพื่อให้ง่ายขึ้น

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

และคอนสตรัคเตอร์นี้ได้รับการสืบทอดดังนั้นคุณสามารถสืบทอดเช่น

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

1
นี่มันเจ๋งมาก. ฉันชอบตัวอย่างสุดท้ายโดยเฉพาะ เป็นเรื่องน่าเสียดายที่เราไม่สามารถหลีกเลี่ยงPackage.__new__()วิธีการต้มสี่บรรทัดได้ หรือบางทีเราสามารถ เราอาจจะกำหนดทั้งมัณฑนากรชั้นเรียนหรือ metaclass ที่สร้างมาตรฐานสำหรับเรา อาหารสำหรับความคิด Pythonic
เซซิลแกงกะหรี่

@CecilCurry ขอบคุณและจุดดี คลาสใดก็ตามที่สืบทอดมาPackageควรทำเช่นนี้ (แม้ว่าฉันยังไม่ได้ทดสอบ) ดังนั้นจึงไม่จำเป็นต้องใช้เมตาคลาส แม้ว่าฉันได้พบวิธีการบางอย่างอยากรู้อยากเห็นสวยใช้งาน metaclasses ในอดีต ...
โทเบียส KIENZLER

@CecilCurry จริงสร้างคือการสืบทอดเพื่อให้คุณสามารถใช้Package(หรือดีกว่าระดับที่ชื่อClosing) objectเป็นผู้ปกครองชั้นเรียนของคุณแทน แต่อย่าถามฉันว่ามรดกหลายอย่างเกิดขึ้นกับเรื่องนี้ได้ยัง
ไง

17

ฉันไม่คิดว่าเป็นไปได้ที่สมาชิกอินสแตนซ์ที่จะลบก่อนหน้า__del__นี้จะถูกเรียก ฉันเดาว่าเหตุผลสำหรับ AttributeError ของคุณนั้นอยู่ที่อื่น (บางทีคุณอาจลบไฟล์ self.file ที่อื่น)

อย่างไรก็ตามในขณะที่คนอื่น ๆ __del__ชี้ให้เห็นคุณควรหลีกเลี่ยงการใช้ เหตุผลหลักสำหรับกรณีนี้คืออินสแตนซ์ที่มี__del__จะไม่ถูกรวบรวมขยะ (พวกเขาจะได้รับอิสรภาพเมื่อ refcount ถึง 0) ดังนั้นหากอินสแตนซ์ของคุณเกี่ยวข้องกับการอ้างอิงแบบวงกลมพวกเขาจะอยู่ในหน่วยความจำตราบเท่าที่แอปพลิเคชันทำงาน (ฉันอาจจะเข้าใจผิดเกี่ยวกับสิ่งนี้แม้ว่าฉันจะต้องอ่านเอกสาร gc อีกครั้ง แต่ฉันค่อนข้างแน่ใจว่ามันทำงานเช่นนี้)


5
วัตถุที่มี__del__ขยะสามารถรวบรวมได้หากการอ้างอิงนับจากวัตถุอื่นที่มี__del__ค่าเป็นศูนย์และไม่สามารถเข้าถึงได้ ซึ่งหมายความว่าหากคุณมีวัฏจักรอ้างอิงระหว่างวัตถุที่มีวัตถุ__del__เหล่านั้นจะไม่ถูกรวบรวม อย่างไรก็ตามกรณีอื่นควรได้รับการแก้ไขตามที่คาดไว้
Collin

"การเริ่มต้นด้วย Python 3.4 วิธี __del __ () ไม่ป้องกันรอบการอ้างอิงจากการรวบรวมขยะอีกต่อไปและโมดูล globals ไม่ได้ถูกบังคับให้ไม่มีในระหว่างการปิดล่ามดังนั้นรหัสนี้ควรทำงานโดยไม่มีปัญหาใด ๆ บน CPython" - docs.python.org/3.6/library/…
Tomasz Gandor

14

ทางเลือกที่ดีคือการใช้weakref.finalize ดูตัวอย่างที่Finalizer วัตถุและfinalizers เปรียบเทียบกับ __del __ () วิธี


1
ใช้งานได้แล้ววันนี้และใช้งานได้ดีกว่าโซลูชั่นอื่น ๆ ฉันมีคลาสเครื่องมือสื่อสารที่ใช้มัลติโพรเซสเซอร์ซึ่งเปิดพอร์ตอนุกรมจากนั้นฉันมีstop()วิธีปิดพอร์ตและjoin()กระบวนการ อย่างไรก็ตามถ้าโปรแกรมออกโดยไม่คาดหมายstop()ไม่ได้เรียก - ฉันแก้ไขด้วย finalizer แต่ในกรณีใด ๆ ฉันโทร_finalizer.detach()ในวิธีการหยุดเพื่อป้องกันการโทรสองครั้ง (ด้วยตนเองและในภายหลังโดย finalizer อีกครั้ง)
Bojan P.

3
IMO นี่เป็นคำตอบที่ดีที่สุดจริงๆ มันรวมความเป็นไปได้ของการทำความสะอาดที่เก็บขยะกับความเป็นไปได้ของการทำความสะอาดที่ทางออก ข้อแม้คือไพ ธ อน 2.7 ไม่มีจุดอ่อนสุดท้าย
hlongmore

12

ฉันคิดว่าปัญหาอาจเกิดขึ้นใน__init__กรณีที่มีรหัสมากกว่าที่แสดง?

__del__จะถูกเรียกแม้ว่า__init__จะไม่ได้ดำเนินการอย่างถูกต้องหรือมีข้อยกเว้น

แหล่ง


2
ฟังดูมีโอกาสมาก วิธีที่ดีที่สุดในการหลีกเลี่ยงปัญหานี้เมื่อใช้งาน__del__คือการประกาศให้สมาชิกทุกคนในระดับชั้นเรียนรู้อย่างชัดเจนว่าพวกเขามีอยู่เสมอแม้ว่าจะ__init__ล้มเหลว ในตัวอย่างที่กำหนดfiles = ()จะทำงานแม้ว่าส่วนใหญ่คุณควรที่จะกำหนดเพียงNone; __init__ในทั้งสองกรณีคุณยังคงต้องกำหนดมูลค่าที่แท้จริงใน
SørenLøvborg

11

นี่คือโครงกระดูกการทำงานที่น้อยที่สุด:

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()

สำคัญ: คืนตัวเอง


หากคุณชอบฉันและมองข้ามreturn selfส่วน (ของคำตอบที่ถูกต้องของClint Miller ) คุณจะจ้องมองเรื่องไร้สาระนี้:

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

หวังว่ามันจะช่วยให้คนต่อไป


8

เพียงแค่หุ้ม destructor ของคุณด้วยคำสั่งลอง / ยกเว้นและมันจะไม่ส่งข้อยกเว้นถ้าคุณกำจัดทิ้งไปแล้ว

แก้ไข

ลองสิ่งนี้:

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

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


2
ปัญหาคือว่าถ้าข้อมูลสมาชิกหายไปมันก็สายเกินไปสำหรับฉัน ฉันต้องการข้อมูลนั้น ดูรหัสของฉันด้านบน: ฉันต้องการชื่อไฟล์เพื่อทราบว่าต้องลบไฟล์ใด ฉันทำให้รหัสของฉันง่ายขึ้น แต่มีข้อมูลอื่นที่ฉันจำเป็นต้องทำความสะอาดตัวเอง (เช่นล่ามจะไม่ทราบวิธีการทำความสะอาด)
wilhelmtell

4

ดูเหมือนว่าวิธีสำนวนที่จะทำคือการให้close()วิธีการ (หรือคล้ายกัน) และเรียกมันอย่างชัดเจน


20
นี่คือวิธีที่ฉันใช้ก่อนหน้านี้ แต่ฉันพบปัญหาอื่น ๆ ด้วย ด้วยข้อยกเว้นที่ถูกโยนทิ้งไปทั่วสถานที่โดยห้องสมุดอื่นฉันต้องการความช่วยเหลือจาก Python ในการทำความสะอาดในกรณีที่เกิดข้อผิดพลาด โดยเฉพาะฉันต้องการ Python เพื่อโทรหา destructor สำหรับฉันเพราะไม่เช่นนั้นรหัสจะไม่สามารถจัดการได้อย่างรวดเร็วและฉันจะลืมจุดออกจากจุดที่ควรจะเรียกว่า. close ()
wilhelmtell
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.