2010-02-04 58 views
23

Nhiều lần thử đã được thực hiện trong quá khứ để thêm chức năng hết thời gian chờ trong Python sao cho khi hết hạn thời gian đã xác định, mã chờ có thể di chuyển. Thật không may, công thức nấu ăn trước đó hoặc cho phép các chức năng chạy để tiếp tục chạy và tiêu thụ tài nguyên hoặc người nào khác bị giết chức năng bằng cách sử dụng một phương pháp cụ thể nền tảng của thread chấm dứt. Mục đích của wiki này là phát triển một câu trả lời đa nền tảng cho vấn đề này mà nhiều lập trình viên đã phải giải quyết cho các dự án lập trình khác nhau.Cách thêm thời gian chờ vào một hàm trong Python

#! /usr/bin/env python 
"""Provide way to add timeout specifications to arbitrary functions. 

There are many ways to add a timeout to a function, but no solution 
is both cross-platform and capable of terminating the procedure. This 
module use the multiprocessing module to solve both of those problems.""" 

################################################################################ 

__author__ = 'Stephen "Zero" Chappell <[email protected]>' 
__date__ = '11 February 2010' 
__version__ = '$Revision: 3 $' 

################################################################################ 

import inspect 
import sys 
import time 
import multiprocessing 

################################################################################ 

def add_timeout(function, limit=60): 
    """Add a timeout parameter to a function and return it. 

    It is illegal to pass anything other than a function as the first 
    parameter. If the limit is not given, it gets a default value equal 
    to one minute. The function is wrapped and returned to the caller.""" 
    assert inspect.isfunction(function) 
    if limit <= 0: 
     raise ValueError() 
    return _Timeout(function, limit) 

class NotReadyError(Exception): pass 

################################################################################ 

def _target(queue, function, *args, **kwargs): 
    """Run a function with arguments and return output via a queue. 

    This is a helper function for the Process created in _Timeout. It runs 
    the function with positional arguments and keyword arguments and then 
    returns the function's output by way of a queue. If an exception gets 
    raised, it is returned to _Timeout to be raised by the value property.""" 
    try: 
     queue.put((True, function(*args, **kwargs))) 
    except: 
     queue.put((False, sys.exc_info()[1])) 

class _Timeout: 

    """Wrap a function and add a timeout (limit) attribute to it. 

    Instances of this class are automatically generated by the add_timeout 
    function defined above. Wrapping a function allows asynchronous calls 
    to be made and termination of execution after a timeout has passed.""" 

    def __init__(self, function, limit): 
     """Initialize instance in preparation for being called.""" 
     self.__limit = limit 
     self.__function = function 
     self.__timeout = time.clock() 
     self.__process = multiprocessing.Process() 
     self.__queue = multiprocessing.Queue() 

    def __call__(self, *args, **kwargs): 
     """Execute the embedded function object asynchronously. 

     The function given to the constructor is transparently called and 
     requires that "ready" be intermittently polled. If and when it is 
     True, the "value" property may then be checked for returned data.""" 
     self.cancel() 
     self.__queue = multiprocessing.Queue(1) 
     args = (self.__queue, self.__function) + args 
     self.__process = multiprocessing.Process(target=_target, 
               args=args, 
               kwargs=kwargs) 
     self.__process.daemon = True 
     self.__process.start() 
     self.__timeout = self.__limit + time.clock() 

    def cancel(self): 
     """Terminate any possible execution of the embedded function.""" 
     if self.__process.is_alive(): 
      self.__process.terminate() 

    @property 
    def ready(self): 
     """Read-only property indicating status of "value" property.""" 
     if self.__queue.full(): 
      return True 
     elif not self.__queue.empty(): 
      return True 
     elif self.__timeout < time.clock(): 
      self.cancel() 
     else: 
      return False 

    @property 
    def value(self): 
     """Read-only property containing data returned from function.""" 
     if self.ready is True: 
      flag, load = self.__queue.get() 
      if flag: 
       return load 
      raise load 
     raise NotReadyError() 

    def __get_limit(self): 
     return self.__limit 

    def __set_limit(self, value): 
     if value <= 0: 
      raise ValueError() 
     self.__limit = value 

    limit = property(__get_limit, __set_limit, 
        doc="Property for controlling the value of the timeout.") 

Edit: Mã này được viết cho Python 3.x và không được thiết kế cho các phương pháp lớp học như một trang trí. Mô-đun multiprocessing không được thiết kế để sửa đổi các phiên bản lớp trên các ranh giới quy trình.

+0

Xử lý ngoại lệ đó chỉ hoạt động trong Python 3. Trong 2.x, nó sẽ vứt bỏ dấu vết ngăn xếp ban đầu, hiển thị ngoại lệ khi bắt nguồn từ "tăng", và khẳng định sẽ không hiển thị trong dấu vết ngăn xếp. –

Trả lời

13

Vấn đề chính với mã của bạn là lạm dụng phòng ngừa xung đột không gian tên đôi gạch dưới trong một lớp không có ý định được phân lớp ở tất cả.

Nói chung, self.__foo là một mùi mã nên được kèm theo một nhận xét dọc theo các dòng # This is a mixin and we don't want arbitrary subclasses to have a namespace conflict.

Tiếp tục API khách hàng của phương pháp này sẽ trông như thế này:

def mymethod(): pass 

mymethod = add_timeout(mymethod, 15) 

# start the processing  
timeout_obj = mymethod() 
try: 
    # access the property, which is really a function call 
    ret = timeout_obj.value 
except TimeoutError: 
    # handle a timeout here 
    ret = None 

Đây không phải là rất pythonic ở tất cả và một api khách hàng tốt hơn sẽ là:

@timeout(15) 
def mymethod(): pass 

try: 
    my_method() 
except TimeoutError: 
    pass 

Bạn đang sử dụng @property trong lớp học của bạn cho một cái gì đó là một nhà nước đột biến accessor, đây không phải là một ý tưởng tốt. Ví dụ, điều gì sẽ xảy ra khi .value được truy cập hai lần? Dường như nó sẽ thất bại vì queue.get() sẽ trả về thùng rác vì hàng đợi đã rỗng.

Xóa hoàn toàn @property. Không sử dụng nó trong bối cảnh này, nó không thích hợp cho trường hợp sử dụng của bạn. Gọi số gọi số khi được gọi và trả lại giá trị hoặc tự nâng cao ngoại lệ. Nếu bạn thực sự phải có giá trị truy cập sau này, hãy làm cho nó thành một phương thức như .get() hoặc .value().

Mã này cho _target nên được viết lại một chút:

def _target(queue, function, *args, **kwargs): 
    try: 
     queue.put((True, function(*args, **kwargs))) 
    except: 
     queue.put((False, exc_info())) # get *all* the exec info, don't do exc_info[1] 

# then later: 
    raise exc_info[0], exc_info[1], exc_info[2] 

Bằng cách đó stack trace sẽ được duy trì một cách chính xác và hiển thị cho các lập trình viên.

Tôi nghĩ bạn đã thực hiện một cú nứt đầu tiên hợp lý khi viết một thư viện hữu ích, tôi thích việc sử dụng mô-đun xử lý để đạt được mục tiêu.

+0

Dấu gạch dưới kép không phải là cách duy nhất để tiếp cận tạo biến riêng trong Python? Các biến riêng được ưa thích trong lập trình hướng đối tượng thực vì đây là cách đóng gói hoạt động, đúng không? – BillR

+0

@BillR: Python không có biến riêng tư "thực". Ngoại trừ tên mangling của các tên lớp có hai dấu gạch dưới bên ngoài lớp, không có gì khác được thực hiện để thực thi chúng là riêng tư - và bạn có thể dễ dàng có được nó nếu bạn biết cách nó hoạt động. Mặc dù tất cả điều này hoàn toàn có thể viết mã hướng đối tượng bằng cách sử dụng nó, vì vậy việc thực thi đóng gói không phải là điều kiện tiên quyết để làm như vậy trong bất kỳ ngôn ngữ lập trình nào. – martineau

6

Đây là cách để có được cú pháp trang trí Jerub nêu

def timeout(limit=None): 
    if limit is None: 
     limit = DEFAULT_TIMEOUT 
    if limit <= 0: 
     raise TimeoutError() # why not ValueError here? 
    def wrap(function): 
     return _Timeout(function,limit) 
    return wrap 

@timeout(15) 
def mymethod(): pass 
+0

Tôi đã sử dụng cú pháp trang trí trước nhưng sẽ không khuyên bạn nên sử dụng nó trong trường hợp này. –

+0

@NoctisSkytower tại sao bạn không nên giới thiệu một trang trí trong trường hợp này? Bạn cảm thấy thế nào là bất lợi hay rủi ro? –

+0

@tristan: Mã được trang trí nhiều nhất liên quan đến các phương pháp trong lớp học. Dựa trên cách xử lý đa hoạt động trong ví dụ này, bất kỳ thay đổi nào xảy ra trong mã được trang trí sẽ không được phản ánh trong đối tượng gốc. Tất cả các thay đổi ở lại trong quá trình thứ hai, hàm 'add_timeout' sẽ kết thúc. –

1

Thư viện Pebble được thiết kế để cung cấp thực hiện cross-platform có khả năng đối phó với logic vấn đề mà có thể crash, segfault or run indefinitely.

from pebble import concurrent 

@concurrent.process(timeout=10) 
def function(foo, bar=0): 
    return foo + bar 

future = function(1, bar=2) 

try: 
    result = future.result() # blocks until results are ready 
except Exception as error: 
    print("Function raised %s" % error) 
    print(error.traceback) # traceback of the function 
except TimeoutError as error: 
    print("Function took longer than %d seconds" % error.args[1]) 

Trình trang trí hoạt động tốt với phương pháp tĩnh và lớp. Tôi sẽ không khuyên bạn nên trang trí phương pháp tuy nhiên, vì nó là một thực hành khá dễ bị lỗi.

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