3

我有一个应用程序严重依赖一个Context实例,该实例充当执行给定计算的上下文的访问点。

如果我想提供对Context实例的访问权限,我可以:

  1. 依靠global
  2. Context作为参数传递给所有需要它的函数

我宁愿不使用global变量,并且将Context实例传递给所有函数既麻烦又冗长。

您将如何“隐藏,但可以访问”计算Context

例如,想象一下Context根据不同的数据简单地计算行星的状态(位置和速度)。

class Context(object):
 def state(self, planet, epoch):
  """base class --- suppose `state` is meant
     to return a tuple of vectors."""
  raise NotImplementedError("provide an implementation!")

class DE405Context(Context):
"""Concrete context using DE405 planetary ephemeris"""
 def state(self, planet, epoch):
   """suppose that de405 reader exists and can provide
      the required (position, velocity) tuple."""
   return de405reader(planet, epoch)

def angular_momentum(planet, epoch, context):
 """suppose we care about the angular momentum of the planet,
    and that `cross` exists"""
 r, v = context.state(planet, epoch)
 return cross(r, v)

# a second alternative, a "Calculator" class that contains the context
class Calculator(object):

 def __init__(self, context):
  self._ctx = context

 def angular_momentum(self, planet, epoch):
  r, v = self._ctx.state(planet, epoch)
  return cross(r, v)

# use as follows:
my_context = DE405Context()
now = now() # assume this function returns an epoch
# first case:
print angular_momentum("Saturn", now, my_context)
# second case:
calculator = Calculator(my_context)
print calculator.angular_momentum("Saturn", now) 

当然,我可以将所有的操作直接添加到“上下文”中,但感觉不对。

在现实生活中,Context不仅计算行星的位置!它计算更多的东西,并充当大量数据的访问点。

所以,为了让我的问题更简洁:你如何处理需要被许多类访问的对象?

我目前正在探索:python 的上下文管理器,但运气不佳。我还考虑过直接向所有函数动态添加属性“上下文”(函数是对象,因此它们可以拥有对任意对象的访问点),即:

def angular_momentum(self, planet, epoch):
 r, v = angular_momentum.ctx.state(planet, epoch)
 return cross(r, v)

# somewhere before calling anything...
import angular_momentum
angular_momentum.ctx = my_context

编辑

很棒的事情是使用with语句创建“计算上下文”,例如:

 with my_context:
  h = angular_momentum("Earth", now)

当然,如果我简单地写,我已经可以做到这一点:

 with my_context as ctx:
  h = angular_momentum("Earth", now, ctx) # first implementation above

也许这是策略模式的变体?

4

2 回答 2

4

您通常不想在 Python 中“隐藏”任何内容。您可能想向人类读者发出信号,他们应该将其视为“私有”,但这实际上只是意味着“即使您忽略此对象,您也应该能够理解我的 API”,而不是“您无法访问它”。

在 Python 中执行此操作的惯用方法是在它前面加上下划线——并且,如果您的模块可能曾经与 一起使用from foo import *,请添加一个__all__列出所有公共导出的显式全局。同样,这些都不会真正阻止任何人看到您的变量,甚至在import foo.

有关更多详细信息,请参阅PEP 8关于全局变量名称。

一些样式指南建议使用特殊前缀、全大写名称或其他特殊区分标记用于全局变量,但 PEP 8 明确表示约定是相同的,除了__all__和/或前导下划线。

同时,您想要的行为显然是全局变量的行为——每个人都隐式共享和引用的单个对象。试图将它伪装成任何东西都对你没有好处,除非可能通过lint你不应该通过的检查或代码审查。全局变量的所有问题都来自于每个人都隐式共享和引用的单个对象,而不是直接在globals()字典中或类似的东西中,因此任何像样的假全局变量都与真正的全局变量一样糟糕。如果这确实是您想要的行为,请将其设为全局变量。

把它放在一起:

# do not include _context here
__all__ = ['Context', 'DE405Context', 'Calculator', …

_context = Context()

此外,当然,您可能希望将其命名为_global_contextor even _private_global_context,而不是仅_context.

但是请记住,全局变量仍然是模块的成员,而不是整个宇宙的成员,因此即使是 publiccontext仍然会像foo.context客户端代码执行import foo. 这可能正是您想要的。如果您想要一种让客户端脚本导入您的模块然后控制其行为的方法,那么也许foo.context = foo.Context(…)正是正确的方法。当然,这在多线程(或 gevent/coroutine/etc.)代码中不起作用,并且在其他各种情况下也不合适,但如果这不是问题,在某些情况下,这很好。

由于您在评论中提到了多线程:在简单的多线程样式中,您有长时间运行的作业,全局样式实际上工作得很好,只需进行微小的更改 - 将全局替换Contextthreading.local包含Context. 即使在线程池处理小型作业的风格中,它也不会复杂得多。您将上下文附加到每个作业,然后当工作人员从队列中拉出作业时,它将线程本地上下文设置为该作业的上下文。

但是,我不确定多线程是否适合您的应用程序。当您的任务偶尔需要阻塞 IO 并且您希望能够在不停止其他任务的情况下做到这一点时,多线程在 Python 中非常有用——但是,多亏了 GIL,它对于并行化 CPU 工作几乎没有用处,听起来这就是您所要做的'正在寻找。多处理(无论是通过multiprocessing模块还是其他方式)可能更多是您所追求的。使用单独的流程,保持单独的上下文更加简单。(或者,您可以编写基于线程的代码并将其切换到多处理,保持threading.local变量不变,只更改生成新任务的方式,一切仍然正常。)

在上下文管理器的意义上提供一个“上下文”可能是有意义的,就像标准库decimal模块的外部版本所做的那样,所以有人可以写:

with foo.Context(…):
    # do stuff under custom context
# back to default context

然而,没有人能真正想到一个好的用例(尤其是因为,至少在幼稚的实现中,它实际上并没有解决线程/等问题),所以它没有被添加到标准库中,并且你可能也不需要它。

如果你想这样做,这很简单。如果您使用的是私有全局,只需将其添加到您的Context课程中:

def __enter__(self):
    global _context
    self._stashedcontext = _context
    _context = self
def __exit__(self, *args):
    global context
    _context = self._stashedcontext

并且应该很明显如何将其调整为公共、线程本地等替代方案。

另一种选择是让一切都成为Context对象的成员。顶层模块函数然后只是委托给具有合理默认值的全局上下文。这正是标准库random模块的工作方式——你可以创建 arandom.Random()并调用randrange它,或者你可以只调用,它在全局默认对象random.randrange()上调用相同的东西。random.Random()

如果在导入时创建 aContext太繁重,特别是如果它可能不会被使用(因为没有人可能会调用全局函数),您可以使用单例模式在首次访问时创建它。但这很少是必要的。如果不是,代码是微不足道的。例如,从第 881 行开始的源代码random是这样的:

_inst = Random()
seed = _inst.seed
random = _inst.random
uniform = _inst.uniform
…

这就是它的全部。

最后,正如您所建议的,您可以使所有内容成为Calculator拥有Context对象的不同对象的成员。这是传统的 OOP 解决方案;过度使用它往往会让 Python 感觉像 Java,但在适当的时候使用它并不是一件坏事。

于 2013-01-03T01:46:30.927 回答
2

您可能会考虑使用代理对象,这是一个有助于创建对象代理的库:

http://pypi.python.org/pypi/ProxyTypes

Flask 为它的“current_app”、“request”和其他变量使用对象代理,引用它们所需要的只是:

from flask import request

您可以创建一个对您的真实上下文的引用的代理对象,并使用线程本地来管理实例(如果这对您有用)。

于 2013-01-03T01:40:51.090 回答