【问题标题】:Python Object Lifetime CharacteristicsPython 对象生命周期特征
【发布时间】:2012-08-10 00:50:48
【问题描述】:

注意:如果您知道任何(非详尽的)库代码可以满足我的要求,请启发 C/C++ 程序员,我会接受它作为答案。

我有一个全局变量设置为以下类的一个实例。其目的是允许我设置一些手动中断点,以便在 scrapy 蜘蛛中放置一些快速而肮脏的printf 样式调试点(我特别需要在满足某些条件来调整解析器时中断,有一些极其罕见的输入数据异常)——改编自this

操作系统是 OS X 10.8。

import termios, fcntl, sys, os

class DebugWaitKeypress(object):
    def __init__(self):
        self.fd = sys.stdin.fileno()
        self.oldterm = termios.tcgetattr(self.fd)
        self.newattr = termios.tcgetattr(self.fd)
        self.newattr[3] = self.newattr[3] & ~termios.ICANON & ~termios.ECHO
        termios.tcsetattr(self.fd, termios.TCSANOW, self.newattr)

        self.oldflags = fcntl.fcntl(self.fd, fcntl.F_GETFL)
        fcntl.fcntl(self.fd, fcntl.F_SETFL, self.oldflags | os.O_NONBLOCK)

    def wait(self):
        sys.stdin.read(1)

    def __del__(self):
        print "called del"
        termios.tcsetattr(self.fd, termios.TCSAFLUSH, self.oldterm)
        fcntl.fcntl(self.fd, fcntl.F_SETFL, self.oldflags)

当我按下 Ctrl-C 并且进程正在展开时,我得到以下异常:

Exception AttributeError: "'NoneType' object has no attribute 'tcsetattr'" in <bound method DebugWaitKeypress.__del__ of <hon.spiders.custom_debug.DebugWaitKeypress object at 0x108985e50>> ignored

我想我错过了一些关于对象生命周期机制的东西?怎么补救这种情况。 AFAIK 任何类实例都应该在导入代码之前被销毁,不是吗?按照声明/定义的相反顺序。

如果在进程退出后终端没有搞砸,我会忽略这一点:D

编辑:

Delian 对 seth 回答的评论让我明白我需要使用 C main() 类似函数,或任何其他作为根函数占主导地位的函数/生成器并在那里初始化上下文。这样,当进程停止时,上下文管理器的__exit__ 方法将被调用。而且我不必在每次wait() 调用时重新编程终端流。

尽管重新编程的成本可能无关紧要,但最好知道如何在 python 中使用这些基本的 C/C++ 语义。

编辑 2:

在与标准输入混淆时,Twisted(scrapy 使用)会变得很糟糕。所以我不得不解决文件IO的问题。

【问题讨论】:

    标签: python oop posix scrapy


    【解决方案1】:

    长话短说:__del__ 对此毫无用处(以及几乎任何其他用途;您可能应该忘记它的存在)。如果您想要确定性清理,请使用上下文管理器。

    AFAIK 任何类实例都应该在导入代码之前被销毁,不是吗?按照声明/定义的相反顺序。

    那是 C++。忘了它。 Python 不关心这一点,事实上它甚至不关心大多数需要做的事情。整个 Python 语言中没有声明之类的东西,模块级变量存储在本质上是一个无序关联数组中。变量不存储对象,它们存储引用(不是 C++ 引用,它们基本上是没有指针算法的指针)——对象在堆上,不知道关于变量、绑定的事情、语句或语句顺序。

    此外,当一个对象被垃圾回收时,它是否完全被 gc'd 是未定义的。由于引用计数,您在 CPython 中获得了大部分确定性的图片,但即使在那里,它也会在您有周期的第二秒下降。结果是__del__ 可能在任何时间点被调用(包括当模块的一半已经被拆除时)或根本不被调用。多个定义 __del__ 的对象相互引用也很麻烦,尽管一些 GC 努力做正确的事情。

    底线是,您可以在 __del__ 运行时假设很少,因此您不能做很多事情。您可以最后一次处理本应通过另一种方法清理但没有清理的资源,仅此而已。经验法则:永远不要依赖它来完成任何事情强制性。

    改为创建一个context manager and use it via with。您可以进行确定性清理,而无需担心对象的生命周期。因为,说实话,对象生命周期和资源生命周期是两个完全不同的东西,并且只纠缠在 C++ 中,因为它是在那种环境中进行资源管理的最佳方式。在 Python 中,RAII 不适用,取而代之的是:

    with <context manager> as var:
        # do something
    # "context closed", whatever that means - for resources, usually cleanup
    

    顺便说一句,您可以通过contextlib 更方便地定义它(从您的版本快速音译,可能包含错误或丑陋):

    from contextlib import contextmanager
    
    
    @contextmanager
    def debug_wait_keypress():
        fd = sys.stdin.fileno()
        oldterm = termios.tcgetattr(fd)
        newattr = termios.tcgetattr(fd)
        newattr[3] = newattr[3] & ~termios.ICANON & ~termios.ECHO
        termios.tcsetattr(fd, termios.TCSANOW, newattr)
        oldflags = fcntl.fcntl(fd, fcntl.F_GETFL)
        fcntl.fcntl(fd, fcntl.F_SETFL, oldflags | os.O_NONBLOCK)
        try:
            yield
        finally:
            termios.tcsetattr(fd, termios.TCSAFLUSH, oldterm)
            fcntl.fcntl(fd, fcntl.F_SETFL, oldflags)
    

    您的wait 方法变为免费函数。

    【讨论】:

      【解决方案2】:

      如果调用__del__,它会在对象的引用计数为零之后的某个时间发生,并且可能直到程序结束,并且没有任何特定的顺序。您也不能依赖__del__ 中可用的任何外部(尤其是全局变量)。

      在您的情况下,python 在调用 DebugWaitKeyPress.__del__ 之前清理了对 termios 模块的引用。这就是您收到'NoneType' object has no attribute 'tcsetattr' 消息的原因。 termios 在您尝试使用它时是 None

      我猜你最好实现一个context manager,并将你的__del__代码放在__exit__中。

      那么你就可以这样说:

      with DebugWaitKeypress(...) as thing:
          do_something_with_it(thing)
      # here, __exit__() is called to do cleanup
      

      来自object.__del__ docs

      由于 __del__() 方法的不稳定情况 调用,在执行期间发生的异常将被忽略,并且 而是将警告打印到 sys.stderr。此外,当 __del__() 是 响应模块被删除而调用(例如,当执行 程序完成),__del__() 方法引用的其他全局变量 可能已经被删除或正在被拆除 (例如进口机器关闭)。因此,__del__() 方法应该做保持外部所需的绝对最低限度 不变量。从 1.5 版开始,Python 保证全局变量 名称以下划线开头的将被删除 删除其他全局变量之前的模块;如果没有其他参考 存在这样的全局变量,这可能有助于确保导入的模块 在调用 __del__() 方法时仍然可用。

      【讨论】:

      • 嗯,上下文管理器还有其他可能的构造吗?该文档似乎建议将该构造用于语句块。这意味着每次我想打断点时,我都必须重新配置终端两次。
      • @HassanSyed 你似乎低估了它。由于 anything (甚至是该死的导入和类定义,尽管您很难找到一个好的用例)可以进入上下文管理器,例如,您可以将调用包装到您的main 功能合二为一,它会在任何事情发生之前配置终端,并且仅在留下main 时重置配置(通常由于异常)。就像您可以在 C++ 中的 main 中的堆栈上创建一个 DebugWaitKeypress
      • 嗯,我认为这就是我需要的洞察力。我可以在作为生成器实现的scrapy的parse方法期间使用该类初始化一个全局,我在生成器顶部启动上下文,然后当我用完url返回生成器超出范围时,使用在scrapy开始关闭之前。
      猜你喜欢
      • 2015-04-22
      • 2012-12-26
      • 2017-01-18
      • 2019-06-17
      • 2017-11-08
      • 1970-01-01
      • 1970-01-01
      • 2011-04-28
      • 1970-01-01
      相关资源
      最近更新 更多