创建对经常访问的对象的挂钩

时间:2013-01-03 01:32:36

标签: python

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

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

  1. 依赖global
  2. Context作为参数传递给所有需要它的函数
  3. 我不想使用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的上下文管理器,但没有太多运气。我还考虑过直接向所有函数动态添加属性“context”(函数是对象,因此它们可以有任意对象的访问点),即:

    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
    

    可能是Strategy pattern的变体吗?

2 个答案:

答案 0 :(得分: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_context甚至_private_global_context,而不仅仅是_context

但请记住,全局变量仍然是模块的成员,而不是整个Universe的成员,所以当客户端代码执行{{1}时,即使是公共context仍然会定位为foo.context }。这可能正是你想要的。如果您想要一种方法让客户端脚本导入模块然后控制其行为,那么import foo可能是正确的方法。当然,这不会在多线程(或gevent / coroutine / etc。)代码中工作,并且在其他各种情况下都不合适,但如果这不是问题,在某些情况下,这个很好。

因为你在评论中提出了多线程:在简单的多线程风格中,你有长时间运行的工作,全局风格实际上非常好,通过一个微不足道的变化 - 用全局替换全局foo.context = foo.Context(…) threading.local包含Context的实例。即使在线程池处理小作业的风格中,也不会复杂得多。您将上下文附加到每个作业,然后当工作人员从队列中取出作业时,它会将线程本地上下文设置为该作业的上下文。

但是,我不确定多线程是否适合您的应用。当您的任务偶尔必须阻止IO并且您希望能够在不停止其他任务的情况下执行此操作时,多线程在Python中很棒 - 但是,由于GIL,它几乎无用于并行化CPU工作,并且它听起来喜欢你正在寻找的东西。多处理(无论是通过Context模块还是其他方式)可能更符合您的要求。通过单独的流程,保持单独的上下文更加简单。 (或者,您可以编写基于线程的代码并将其切换为多处理,将multiprocessing变量保留为原样,只更改生成新任务的方式,一切都运行正常。)

提供一个" context"可能是有意义的。在上下文管理器意义上,作为标准库的decimal模块的外部版本,所以有人可以写:

threading.local

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

如果你想这样做,那就非常微不足道了。如果您正在使用私有全局,只需将其添加到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 对象的成员。然后,顶级模块函数只委托给具有合理默认值的全局上下文。这正是标准库random模块的工作原理 - 您可以创建Context并在其上调用random.Random(),或者您可以调用randrange,它调用相同的内容在全局默认random.randrange()对象上。

如果创建random.Random()太重而无法在导入时执行,特别是如果它可能不会被使用(因为没有人可能会调用全局函数),您可以使用单例模式在首次访问时创建它。但这很少需要。如果不是,那么代码就是微不足道的。例如,从第881行开始的sourceContext执行此操作:

random

这就是它的全部内容。

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

答案 1 :(得分:2)

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

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

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

from flask import request

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