使用单例模式作为计数器

3

我有一个自动化测试,使用一个函数来创建截图到一个文件夹中。多个截图实例都会调用该函数。在每次测试运行时,都会创建一个新的文件夹,因此我不需要考虑计数器重置的问题。为了反映这些截图的拍摄顺序,我需要想出可以按顺序排序的名称。这是我的解决方案:

def make_screenshot_file(file_name):
    order = Counter().count
    test_suites_path = _make_job_directory()
    return make_writable_file(os.path.join(test_suites_path,'screenshot',file_name % order))


class Counter():
    __counter_instance = None

    def __init__(self):
        if Counter.__counter_instance is None:
            self.count = 1
            Counter.__counter_instance = self
        else: 
            Counter.__counter_instance.count += 1
            self.count =  Counter.__counter_instance.count

对我来说这很好用。但是我一直在想,是否有更简单的方法来解决这个问题呢?如果单例是唯一的方法,那么我的代码能否进行优化呢?


这实际上根本不是单例。您创建了许多Counter实例,它们都指向一个“特殊实例”。这与仅拥有一个实例不同。 - abarnert
此外,如果您已经了解足够的类属性知识来创建一个名为__counter_instance的属性,为什么不直接创建一个count类属性呢? - abarnert
@abarnert,抱歉,我不理解你的第二条评论。你能展示一下整个代码是什么样子吗? - Prostak
4个回答

4
你在这里尝试模拟全局变量。
没有好的理由这样做。如果你真的想要一个全局变量,就明确地将它设为全局变量。
你可以创建一个简单的Counter类,每次访问时将count增加1,并创建一个全局实例。但标准库已经免费为你提供了类似的功能,在itertools.count中,正如DSM在评论中所解释的那样。
所以:
import itertools

_counter = itertools.count()
def make_screenshot_file(file_name):
    order = next(_counter)
    test_suites_path = _make_job_directory()
    return make_writable_file(os.path.join(test_suites_path,'screenshot',file_name % order))

我不确定为什么你如此担心存储空间或时间,因为我无法想象有任何程序会在使用8个字节或800个字节的单个对象时产生影响,而且你也只会访问它几次,无论访问速度是3ns还是3us都不会有太大区别。
但如果你确实担心,正如你可以从源代码中看到的那样,count是用C实现的,它非常节省内存,如果你不对其进行任何高级操作,每个数字基本上只需要一个PyNumber_Add就能生成,这比解释几行代码要少得多。

既然你问了,这是如何通过使用_count类属性来彻底简化你现有的代码而不是__counter_instance类属性:

class Counter():
    _count = 0
    def count(self):
        Counter._count += 1
        return Counter.count

当然现在你需要使用Counter().count()而不是仅使用Counter().count——但是如果有必要,您可以使用@property轻松解决这个问题。值得指出的是,使用经典类而不是新式类(在括号内不传递任何内容)是一个非常糟糕的想法,如果您确实想要一个经典类,则应该省略括号,并且大多数Python程序员将把名称Counter与类collections.Counter相关联,而没有理由count不能是@classmethod@staticmethod...此时这正是Andrew T.的答案。正如他所指出的那样,这比您正在做的要简单得多,也不更具Python特色。但是真正的问题是,所有这些都不比使_count成为模块级全局变量并添加模块级count()函数来增加和返回它更好。

2

为什么不直接这样做呢?

order = time.time()

或者做一些类似的事情。
import glob #glob is used for unix like path expansion
order = len(glob.glob(os.path.join(test_suites_path,"screenshot","%s*"%filename))

返回-1。也许OP不应该这样做,因为这是一个自动化测试。如果这个自动化测试同时从多台机器上运行,或者可能在同一台机器的不同线程/进程中同时运行,那么他有可能会得到不一致的结果。 - GodMan
因为test_suites_path已经在名称中包含了时间。我不想在路径中有两个时间名称逻辑。 - Prostak
@JoranBeasley:同样的论点再次适用。如果两个文件名中的时间相同,则写入文件的数据仅属于一个文件,而不是两个文件 :) - GodMan

1
使用静态方法和变量。不太符合Python的风格,但更简单。
def make_screenshot_file(file_name):
    order = Counter.count() #Note the move of the parens
    test_suites_path = _make_job_directory()
    return make_writable_file(os.path.join(test_suites_path,'screenshot',file_name % order))

class Counter():
  count_n = 0

  @staticmethod
  def count():
    Counter.count_n += 1
    return Counter.count_n


print Counter.count()
print Counter.count()
print Counter.count()
print Counter.count()
print Counter.count()


atarzwell@freeman:~/src$ python so.py
1
2
3
4
5

3
您可以使用 C = itertools.count(1) 然后 order = next(C)。该代码用途为生成一个计数器对象C,并从中获取下一个值作为排序顺序order - DSM
@Andrew T,那么您的解决方案会比我的更快,对吗? - Prostak
@Prostak:谁在意它数到5的速度有多快? - abarnert
@abarnert,只想让它尽可能地精简。使用的内存越少,越好。没有内存泄漏。而且,速度越快,越好。 - Prostak
@abarnert,到目前为止我最喜欢你的答案。非常简洁。把它发表出来,我会给你点赞的。 - Prostak
显示剩余2条评论

0

好的,你可以使用这个解决方案,只要确保你从不初始化 order kwarg!

函数中的可变关键字参数(Mutable Kwargs)工作方式类似于类全局变量。它的值在调用之间不会像你一开始想的那样重置为默认值!

def make_screenshot_file(file_name , order=[0]):
    order[0] = order[0] + 1
    test_suites_path = _make_job_directory()
    return make_writable_file(os.path.join(test_suites_path,'screenshot',file_name % order[0]))

您是否真的尝试过这样做?这将每次返回1。该值并未“重置为默认值”,但这仅仅是因为该值从未更改过。 order+=1并不改变值0,它只是将一个新值0+1绑定到局部名称order。下一次,您将再次将0+1绑定到局部名称order。以此类推。 - abarnert
@abarnert,我的错...它只适用于可变对象,修复了代码。感谢指出我的错误。 - andsoa
为什么你会写 order.append(order.pop()+1) 而不是直接写 order[0] += 1 - abarnert
你太快了!!!我被UnboundLocalError问题搞糊涂了,这完全是无关的。 - andsoa
@andsoa,当我尝试了你的第一个答案后,它没有起作用。所以我排除了你。但是当我尝试了你修改过的答案时,哇,它起作用了。非常印象深刻,也学到了一个新技巧。谢谢!+1 - Prostak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接