2015-06-23 13 views
7

Những gì tôi cần phải thực hiện:Ngăn chặn TextIOWrapper từ đóng trên GC trong một/cách Py2 Py3 tương thích

Cho một tập tin nhị phân, giải mã nó trong một vài cách khác nhau cung cấp một API TextIOBase. Lý tưởng nhất là các tệp tiếp theo này có thể được truyền mà không cần phải theo dõi tuổi thọ của chúng một cách rõ ràng.

Thật không may, việc gói BufferedReader sẽ dẫn đến việc trình đọc đó bị đóng khi TextIOWrapper nằm ngoài phạm vi.

Đây là một bản demo đơn giản này:

In [1]: import io 

In [2]: def mangle(x): 
    ...:  io.TextIOWrapper(x) # Will get GCed causing __del__ to call close 
    ...:  

In [3]: f = io.open('example', mode='rb') 

In [4]: f.closed 
Out[4]: False 

In [5]: mangle(f) 

In [6]: f.closed 
Out[6]: True 

tôi có thể sửa lỗi này bằng Python 3 bằng cách ghi đè __del__ (đây là một giải pháp hợp lý đối với trường hợp sử dụng của tôi như tôi hoàn toàn kiểm soát quá trình giải mã, tôi chỉ cần để lộ một API rất đồng đều ở cuối):

In [1]: import io 

In [2]: class MyTextIOWrapper(io.TextIOWrapper): 
    ...:  def __del__(self): 
    ...:   print("I've been GC'ed") 
    ...:   

In [3]: def mangle2(x): 
    ...:  MyTextIOWrapper(x) 
    ...:  

In [4]: f2 = io.open('example', mode='rb') 

In [5]: f2.closed 
Out[5]: False 

In [6]: mangle2(f2) 
I've been GC'ed 

In [7]: f2.closed 
Out[7]: False 

Tuy nhiên điều này không làm việc trong Python 2:

In [7]: class MyTextIOWrapper(io.TextIOWrapper): 
    ...:  def __del__(self): 
    ...:   print("I've been GC'ed") 
    ...:   

In [8]: def mangle2(x): 
    ...:  MyTextIOWrapper(x) 
    ...:  

In [9]: f2 = io.open('example', mode='rb') 

In [10]: f2.closed 
Out[10]: False 

In [11]: mangle2(f2) 
I've been GC'ed 

In [12]: f2.closed 
Out[12]: True 

Tôi đã dành một chút thời gian nhìn chằm chằm vào mã nguồn Python và nó trông khá giống nhau giữa 2,7 và 3,4 vì vậy tôi không hiểu tại sao __del__ được thừa hưởng từ IOBase không được ghi đè trong Python 2 (hoặc thậm chí có thể nhìn thấy trong dir), nhưng dường như vẫn bị xử tử. Python 3 hoạt động chính xác như mong đợi.

Tôi có thể làm gì không?

+0

Lý tưởng nhất là tôi muốn tránh vô hiệu hóa GC và bật lại sau đó, nhưng tôi chắc chắn đã nghĩ về điều đó ... – ebolyen

Trả lời

4

Hóa ra là về cơ bản không có gì có thể thực hiện được về trình giải mã gọi close bằng Python 2.7. Mã này được mã hóa thành mã C.Thay vào đó chúng ta có thể sửa đổi close sao cho nó sẽ không đóng bộ đệm khi __del__ xảy ra (__del__ sẽ được thực hiện trước _PyIOBase_finalize trong mã C cho chúng ta cơ hội thay đổi hành vi của close). Điều này cho phép close hoạt động như mong đợi mà không để GC đóng bộ đệm.

class SaneTextIOWrapper(io.TextIOWrapper): 
    def __init__(self, *args, **kwargs): 
     self._should_close_buffer = True 
     super(SaneTextIOWrapper, self).__init__(*args, **kwargs) 

    def __del__(self): 
     # Accept the inevitability of the buffer being closed by the destructor 
     # because of this line in Python 2.7: 
     # https://github.com/python/cpython/blob/2.7/Modules/_io/iobase.c#L221 
     self._should_close_buffer = False 
     self.close() # Actually close for Python 3 because it is an override. 
         # We can't call super because Python 2 doesn't actually 
         # have a `__del__` method for IOBase (hence this 
         # workaround). Close is idempotent so it won't matter 
         # that Python 2 will end up calling this twice 

    def close(self): 
     # We can't stop Python 2.7 from calling close in the deconstructor 
     # so instead we can prevent the buffer from being closed with a flag. 

     # Based on: 
     # https://github.com/python/cpython/blob/2.7/Lib/_pyio.py#L1586 
     # https://github.com/python/cpython/blob/3.4/Lib/_pyio.py#L1615 
     if self.buffer is not None and not self.closed: 
      try: 
       self.flush() 
      finally: 
       if self._should_close_buffer: 
        self.buffer.close() 

giải pháp trước đây của tôi ở đây sử dụng _pyio.TextIOWrapper mà là chậm hơn so với ở trên vì nó được viết bằng Python, không C.

Nó liên quan đến việc chỉ đơn giản là trọng __del__ với một noop mà cũng sẽ làm việc trong Py2/3.

0

Một giải pháp đơn giản là trả về biến từ hàm và lưu nó trong phạm vi tập lệnh, để nó không thu thập rác cho đến khi tập lệnh kết thúc hoặc tham chiếu đến nó thay đổi. Nhưng có thể có các giải pháp thanh lịch khác ngoài kia.

0

EDIT:

Tôi tìm thấy một giải pháp tốt hơn nhiều (tương đối), nhưng tôi sẽ để lại câu trả lời này trong trường hợp nó rất hữu ích cho bất cứ ai để học hỏi từ. (Đó là một cách khá dễ dàng để thể hiện gc.garbage)

Xin vui lòng không thực sự sử dụng những gì sau.

OLD:

Tôi tìm thấy một giải pháp tiềm năng, mặc dù nó là khủng khiếp:

Những gì chúng ta có thể làm là thiết lập một tham chiếu cyclic trong destructor, mà sẽ tổ chức ra khỏi sự kiện GC. Sau đó, chúng tôi có thể xem số garbage của gc để tìm các đối tượng không thể truy cập này, phá vỡ chu kỳ và thả tham chiếu đó.

In [1]: import io 

In [2]: class MyTextIOWrapper(io.TextIOWrapper): 
    ...:  def __del__(self): 
    ...:   if not hasattr(self, '_cycle'): 
    ...:    print "holding off GC" 
    ...:    self._cycle = self 
    ...:   else: 
    ...:    print "getting GCed!" 
    ...: 

In [3]: def mangle(x): 
    ...:  MyTextIOWrapper(x) 
    ...:  

In [4]: f = io.open('example', mode='rb') 

In [5]: mangle(f) 
holding off GC 

In [6]: f.closed 
Out[6]: False 

In [7]: import gc 

In [8]: gc.garbage 
Out[8]: [] 

In [9]: gc.collect() 
Out[9]: 34 

In [10]: gc.garbage 
Out[10]: [<_io.TextIOWrapper name='example' encoding='UTF-8'>] 

In [11]: gc.garbage[0]._cycle=False 

In [12]: del gc.garbage[0] 
getting GCed! 

In [13]: f.closed 
Out[13]: True 

Thật ra đây là một phương pháp khá khủng khiếp, nhưng nó có thể là trong suốt đối với các API tôi cung cấp. Tôi vẫn muốn cách ghi đè số __del__ của IOBase.

Các vấn đề liên quan