如何在Python脚本间共享变量?

42 投票
14 回答
87174 浏览
提问于 2025-04-15 16:34

下面的代码无法正常工作

这是一个文件叫做 one.py

import shared
shared.value = 'Hello'
raw_input('A cheap way to keep process alive..')

这是另一个文件叫做 two.py

import shared
print shared.value

在两个命令行中运行如下:

>>python one.py
>>python two.py

(第二个命令会出现属性错误,这个是对的)。

有没有办法做到这一点,也就是在两个脚本之间共享一个变量呢?

14 个回答

20
sudo apt-get install memcached python-memcache

这是一个名为 one.py 的文件

import memcache
shared = memcache.Client(['127.0.0.1:11211'], debug=0)
shared.set('Value', 'Hello')

这是另一个名为 two.py 的文件

import memcache
shared = memcache.Client(['127.0.0.1:11211'], debug=0)    
print shared.get('Value')
26

你想要的功能是无法实现的,因为你需要把信息存储在两个解释器实例之外的地方。
如果你只是想保存一些简单的变量,可以使用pickle模块把一个Python字典保存到文件中,然后在第二个脚本中重新加载这个文件。
下面是一个例子:

one.py

import pickle

shared = {"Foo":"Bar", "Parrot":"Dead"}
fp = open("shared.pkl","w")
pickle.dump(shared, fp)

two.py

import pickle

fp = open("shared.pkl")
shared = pickle.load(fp)
print shared["Foo"]
59

希望在这里记录一下我对这个问题的笔记。

首先,我非常感谢原帖中的例子,因为这也是我开始的地方——虽然一开始我以为 shared 是某个内置的 Python 模块,直到我在[Tutor] 模块间的全局变量??找到了一个完整的例子。

不过,当我寻找“在脚本之间共享变量”(或进程)时——除了 Python 脚本需要使用其他 Python 源文件中定义的变量(但不一定是正在运行的进程)——我主要遇到了另外两种用例:

  • 一个脚本将自己分叉成多个子进程,这些子进程在同一台电脑上并行运行(可能在多个处理器上)
  • 一个脚本生成多个其他子进程,这些子进程在同一台电脑上并行运行(可能在多个处理器上)

因此,大多数关于“共享变量”和“进程间通信”(IPC)的讨论都是围绕这两种情况;然而,在这两种情况下,都可以观察到一个“父进程”,而“子进程”通常会有一个引用。

然而,我感兴趣的是独立运行同一个脚本的多个实例,并在这些实例之间共享数据(就像Python: 如何在多个脚本调用中共享对象实例中所描述的),以单例/单实例模式运行。这个问题并没有被上述两种情况真正解决——相反,它基本上简化为原帖中的例子(在两个脚本之间共享变量)。

现在,在处理这个问题时,如果使用 Perl,有IPC::Shareable;它“允许你将变量绑定到共享内存”,使用“一个整数或四个字符的字符串[1]作为跨进程空间的数据的共同标识符”。因此,没有临时文件,也没有网络设置——这对我的用例来说非常好;所以我在寻找 Python 中的类似功能。

然而,正如@Drewfer 的接受回答所指出的:“如果不将信息存储在两个解释器实例之外的某个地方,你是无法做到你想要的”;换句话说:你要么必须使用网络/套接字设置,要么必须使用临时文件(因此,对于“完全独立的 Python 会话”没有共享内存)。

即使考虑到这些因素,找到有效的例子还是有点困难(除了 pickle)——在mmapmultiprocessing的文档中也是如此。我找到了一些其他的例子——这些例子也描述了一些文档中没有提到的陷阱:

感谢这些例子,我想出了一个例子,它基本上与 mmap 示例做的相同,采用了“同步 Python 字典”示例中的方法——使用 BaseManager(通过 manager.start() 通过文件路径地址)与共享列表;服务器和客户端都可以读取和写入(见下文)。请注意:

  • multiprocessing 管理器可以通过 manager.start()server.serve_forever() 启动
    • serve_forever() 锁定 - start() 不会
    • multiprocessing 中有自动日志记录功能:它似乎在 start() 的进程中工作良好,但似乎忽略了那些使用 serve_forever() 的进程
  • multiprocessing 中的地址指定可以是 IP(套接字)或临时文件(可能是管道?)路径;在 multiprocessing 文档中:
    • 大多数示例使用 multiprocessing.Manager() - 这只是一个函数(不是 类实例化),返回一个 SyncManager,这是 BaseManager 的一个特殊子类;并使用 start() - 但 用于独立运行脚本之间的 IPC;这里使用的是文件路径
    • 少数其他示例使用 serve_forever() 方法进行独立运行脚本之间的 IPC;这里使用的是 IP/套接字地址
    • 如果没有指定地址,则会自动使用临时文件路径(见16.6.2.12. 日志记录,了解如何查看这个)

除了在“同步 Python 字典”帖子中的所有陷阱外,在列表的情况下还有其他陷阱。该帖子指出:

对字典的所有操作必须使用方法,而不是字典赋值(syncdict["blast"] = 2 会因为 multiprocessing 共享自定义对象的方式而失败)

解决 dict['key'] 获取和设置的问题,是使用 dict 的公共方法 getupdate。问题是,对于 list[index] 并没有这样的公共方法;因此,对于共享列表,我们还必须将 __getitem____setitem__ 方法(这些是 list 的私有方法)注册为 exposed,这意味着我们还必须重新注册所有 list 的公共方法 :/

好吧,我认为这些是最关键的内容;这就是两个脚本——它们可以在不同的终端中运行(先运行服务器);注意是在 Linux 上使用 Python 2.7 开发的:

a.py(服务器):

import multiprocessing
import multiprocessing.managers

import logging
logger = multiprocessing.log_to_stderr()
logger.setLevel(logging.INFO)


class MyListManager(multiprocessing.managers.BaseManager):
    pass


syncarr = []
def get_arr():
    return syncarr

def main():

    # print dir([]) # cannot do `exposed = dir([])`!! manually:
    MyListManager.register("syncarr", get_arr, exposed=['__getitem__', '__setitem__', '__str__', 'append', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort'])

    manager = MyListManager(address=('/tmp/mypipe'), authkey='')
    manager.start()

    # we don't use the same name as `syncarr` here (although we could);
    # just to see that `syncarr_tmp` is actually <AutoProxy[syncarr] object>
    # so we also have to expose `__str__` method in order to print its list values!
    syncarr_tmp = manager.syncarr()
    print("syncarr (master):", syncarr, "syncarr_tmp:", syncarr_tmp)
    print("syncarr initial:", syncarr_tmp.__str__())

    syncarr_tmp.append(140)
    syncarr_tmp.append("hello")

    print("syncarr set:", str(syncarr_tmp))

    raw_input('Now run b.py and press ENTER')

    print
    print 'Changing [0]'
    syncarr_tmp.__setitem__(0, 250)

    print 'Changing [1]'
    syncarr_tmp.__setitem__(1, "foo")

    new_i = raw_input('Enter a new int value for [0]: ')
    syncarr_tmp.__setitem__(0, int(new_i))

    raw_input("Press any key (NOT Ctrl-C!) to kill server (but kill client first)".center(50, "-"))
    manager.shutdown()

if __name__ == '__main__':
  main()

b.py(客户端)

import time

import multiprocessing
import multiprocessing.managers

import logging
logger = multiprocessing.log_to_stderr()
logger.setLevel(logging.INFO)


class MyListManager(multiprocessing.managers.BaseManager):
    pass

MyListManager.register("syncarr")

def main():
  manager = MyListManager(address=('/tmp/mypipe'), authkey='')
  manager.connect()
  syncarr = manager.syncarr()

  print "arr = %s" % (dir(syncarr))

  # note here we need not bother with __str__ 
  # syncarr can be printed as a list without a problem:
  print "List at start:", syncarr
  print "Changing from client"
  syncarr.append(30)
  print "List now:", syncarr

  o0 = None
  o1 = None

  while 1:
    new_0 = syncarr.__getitem__(0) # syncarr[0]
    new_1 = syncarr.__getitem__(1) # syncarr[1]

    if o0 != new_0 or o1 != new_1:
      print 'o0: %s => %s' % (str(o0), str(new_0))
      print 'o1: %s => %s' % (str(o1), str(new_1))
      print "List is:", syncarr

      print 'Press Ctrl-C to exit'
      o0 = new_0
      o1 = new_1

    time.sleep(1)


if __name__ == '__main__':
    main()

最后补充一下,在 Linux 上会创建 /tmp/mypipe——但大小为 0 字节,属性为 srwxr-xr-x(用于套接字);我想这让我很高兴,因为我不需要担心网络端口,也不需要担心临时文件 :)

其他相关问题:

撰写回答