Python对象生命周期特征

时间:2012-08-10 00:50:48

标签: python oop posix scrapy

注意:如果您知道任何(非精细的)库代码可以实现我想要的功能,请启发C / C ++程序员,我会接受它作为答案。

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

Os是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使用)在与stdin混淆时会变成apeshit。所以我不得不用文件IO解决问题。

2 个答案:

答案 0 :(得分:5)

长话短说:__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方法成为免费功能。

答案 1 :(得分:4)

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

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

我猜你最好不要实施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 __()方法时仍然可用。