1. 为什么要用代理模式
代理模式,即使用变量代表函数运行的结果:
class A:
def __init__():
self.a = []
self.b = FactoryProxy(lambda: len(self.a))
例如这里使用了 self.b 代表 self.a 数组中的元素个数, 而且如果改变了 self.a (例如向 self.a 中 append 了内容), self.b 也会同步变化.
另一个使用场景是作为变量容器。如果我们需要在模块级定义一个非可变(例如数组、字典)类型的值,并需要不断更新,我们通常需要使用字典包裹:
module_var = {'': 1}
def a(x):
module_var[''] = x
print(module_var[''])
使用代理模式可以实现更加 Pythonic、可读更高的版本:
module_var = Proxy(1)
def a(x):
module_var.set(x)
print(module_var)
接下来,提供代理模式最常见的使用场景:实现动态全局配置。
2. 动态全局配置的实现
首先,新建一个 config.py:
import os
from pathlib import Path
from typing import TYPE_CHECKING, Union
from box import BoxKeyError, ConfigBox
from loguru import logger
from .utils import ProxyBase
DEFAULT_CONF = {
"host": os.environ.get("_WEB_HOST", "127.0.0.1"),
"port": 2008,
"prefix": "/",
"db": "./githubstar.db",
}
class ConfigError(BoxKeyError):
pass
if TYPE_CHECKING:
Base = ConfigBox
else:
Base = object
class Config(ProxyBase, Base):
__noproxy__ = ("_conf_file", "_cache", "__getitem__")
def __init__(self, conf_file=None):
self._conf_file = conf_file
self._cache = None
@property
def __subject__(self):
if not self._cache:
self.reload_conf(conf_file=self._conf_file)
return self._cache
def reset(self):
self._cache = None
def reload_conf(self, conf_file=None, box=None):
"""Load config from provided file or config.toml at cwd."""
if not box:
box = ConfigBox(DEFAULT_CONF, box_dots=True)
default_conf = Path("./config.toml")
if conf_file:
conf_file = Path(conf_file)
elif self._conf_file:
conf_file = Path(self._conf_file)
elif default_conf.is_file():
conf_file = default_conf
else:
logger.debug(f"No config found from provided file or ./{default_conf}.")
if conf_file:
if conf_file.suffix.lower() == ".toml":
box.merge_update(ConfigBox.from_toml(filename=conf_file))
elif conf_file.suffix.lower() in (".yaml", ".yml"):
box.merge_update(ConfigBox.from_yaml(filename=conf_file))
else:
logger.warning(f'Can not load config file "{conf_file}", a yaml/toml file is required.')
self._conf_file = conf_file
self._cache = box
if conf_file:
logger.debug(f'Now using config file at "{conf_file.absolute()}".')
def __getitem__(self, key):
try:
return self.__subject__[key]
except BoxKeyError:
msg = f'can not find config key "{key}", please check your config file.'
raise ConfigError(msg) from None
config: Union[ConfigBox, Config] = Config()
(ProxyBase 的实现参见第三部分)
这样,我们只需要通过以下代码,即可实现从文件读取 config,然后使用 config 变量访问全局配置:
import typer
from .config import config
cli = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
@cli.command()
def server(
config_file: Path = typer.Argument(
None,
dir_okay=False,
allow_dash=True,
help="Config toml file",
)
):
if config_file:
config.reload_conf(config_file)
print('Host:', config.get('host'))
使用 reload_conf 读取配置文件后,任何模块中都可以通过以下代码导入 config 动态变量:
from .config import config
然后通过 get() 来获取配置:
config.get('host', '<Unknown>')
同时我们也可以增加配置文件修改时自动更新的方法,这里我们使用 watchdog 模块实现文件修改监控:
import functools
from pathlib import Path
from threading import Thread, Event
import time
from typing import Union
from box import BoxError, ConfigBox
from loguru import logger
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler, FileSystemEvent
from .utils import ProxyBase
DEFAULT_CONF = {
"host": os.environ.get("_WEB_HOST", "127.0.0.1"),
"port": 2008,
"prefix": "/",
"db": "./githubstar.db",
}
class ConfigError(BoxError):
pass
class ConfigChangeHandler(FileSystemEventHandler):
def __init__(self, *args, func=None, **kw):
super().__init__(*args, **kw)
self.func = func
def on_modified(self, event: FileSystemEvent):
logger.info(f'Config file has changed, reloading.')
try:
self.func()
except Exception as e:
logger.warning(f'Can not reload config file: {e}.')
class Config(ProxyBase):
__noproxy__ = ("_conf_file", "_cache", "_observer", "__getitem__")
def __init__(self, conf_file=None):
self._conf_file = conf_file
self._cache = None
self._observer = None
@property
def __subject__(self):
if not self._cache:
self.reload_conf(conf_file=self._conf_file)
return self._cache
def reset(self):
self._cache = None
def start_observer(self, conf_file, box):
if self._observer:
self._observer.stop()
self._observer = obs = Observer()
func = functools.partial(self.reload_conf, box = box)
obs.schedule(ConfigChangeHandler(func=func), conf_file)
obs.start()
def reload_conf(self, conf_file=None, box=None):
"""Load config from provided file or config.toml at cwd."""
if not box:
box = ConfigBox(DEFAULT_CONF, box_dots=True)
default_conf = Path("./config.toml")
if conf_file:
conf_file = Path(conf_file)
elif self._conf_file:
conf_file = Path(self._conf_file)
elif default_conf.is_file():
conf_file = default_conf
else:
logger.debug(f"No config found from provided file or ./{default_conf}.")
if conf_file:
if conf_file.suffix.lower() == ".toml":
box.merge_update(ConfigBox.from_toml(filename=conf_file))
elif conf_file.suffix.lower() in (".yaml", ".yml"):
box.merge_update(ConfigBox.from_yaml(filename=conf_file))
else:
logger.warning(f'Can not load config file "{conf_file}", a yaml/toml file is required.')
logger.debug(f'Now using config file at "{conf_file.absolute()}".')
self._conf_file = conf_file
self._cache = box
self.start_observer(conf_file, box)
def __getitem__(self, key):
try:
return self.__subject__[key]
except BoxError:
msg = f'can not find config key "{key}", please check your config file or env var.'
raise ConfigError(msg) from None
config: Union[ConfigBox, Config] = Config()
除此之外,我们还可以增加配置文件自动检查 (使用 schema 模块) 等功能,这里不赘述。
3. 代理模式的实现
新建一个 utils.py:
class ProxyBase:
"""
A proxy class that make accesses just like direct access to __subject__ if not overwriten in the class.
Attributes defined in __noproxy__ will not be proxied to __subject__. Functions and properties will not
be proxied by default.
Example:
```python
class Config(ProxyBase):
__noproxy__ = ("conf_file",)
def __init__(self, conf_file=None):
self.conf_file = conf_file
@property
def __subject__(self):
return self.conf_file.read()
```
"""
__slots__ = ()
def __call__(self, *args, **kw):
return self.__subject__(*args, **kw)
def hasattr(self, attr):
try:
object.__getattribute__(self, attr)
return True
except AttributeError:
return False
def __getattribute__(self, attr, oga=object.__getattribute__):
if attr.startswith("__") and attr not in oga(self, "_noproxy"):
try:
subject = oga(self, "__subject__")
except AttributeError as e:
exc = ProxyAttributeError(e)
exc.with_traceback(e.__traceback__)
raise exc from None
if attr == "__subject__":
return subject
return getattr(subject, attr)
return oga(self, attr)
def __getattr__(self, attr, oga=object.__getattribute__):
if attr == "hasattr" or self.hasattr(attr):
return oga(self, attr)
else:
return getattr(oga(self, "__subject__"), attr)
@property
def _noproxy(self, oga=object.__getattribute__):
base = oga(self, "__class__")
for cls in inspect.getmro(base):
if hasattr(cls, "__noproxy__"):
yield from cls.__noproxy__
def __setattr__(self, attr, val, osa=object.__setattr__):
if attr == "__subject__" or attr in self._noproxy:
return osa(self, attr, val)
return setattr(self.__subject__, attr, val)
def __delattr__(self, attr, oda=object.__delattr__):
if attr == "__subject__" or hasattr(type(self), attr) and not attr.startswith("__"):
oda(self, attr)
else:
delattr(self.__subject__, attr)
def __bool__(self):
return bool(self.__subject__)
def __getitem__(self, arg):
return self.__subject__[arg]
def __setitem__(self, arg, val):
self.__subject__[arg] = val
def __delitem__(self, arg):
del self.__subject__[arg]
def __getslice__(self, i, j):
return self.__subject__[i:j]
def __setslice__(self, i, j, val):
self.__subject__[i:j] = val
def __delslice__(self, i, j):
del self.__subject__[i:j]
def __contains__(self, ob):
return ob in self.__subject__
for name in "repr str hash len abs complex int long float iter".split():
exec("def __%s__(self): return %s(self.__subject__)" % (name, name))
for name in "cmp", "coerce", "divmod":
exec("def __%s__(self, ob): return %s(self.__subject__, ob)" % (name, name))
for name, op in [
("lt", "<"),
("gt", ">"),
("le", "<="),
("ge", ">="),
("eq", " == "),
("ne", "!="),
]:
exec("def __%s__(self, ob): return self.__subject__ %s ob" % (name, op))
for name, op in [("neg", "-"), ("pos", "+"), ("invert", "~")]:
exec("def __%s__(self): return %s self.__subject__" % (name, op))
for name, op in [
("or", "|"),
("and", "&"),
("xor", "^"),
("lshift", "<<"),
("rshift", ">>"),
("add", "+"),
("sub", "-"),
("mul", "*"),
("div", "/"),
("mod", "%"),
("truediv", "/"),
("floordiv", "//"),
]:
exec(
(
"def __%(name)s__(self, ob):\n"
" return self.__subject__ %(op)s ob\n"
"\n"
"def __r%(name)s__(self, ob):\n"
" return ob %(op)s self.__subject__\n"
"\n"
"def __i%(name)s__(self, ob):\n"
" self.__subject__ %(op)s=ob\n"
" return self\n"
)
% locals()
)
del name, op
def __index__(self):
return self.__subject__.__index__()
def __rdivmod__(self, ob):
return divmod(ob, self.__subject__)
def __pow__(self, *args):
return pow(self.__subject__, *args)
def __ipow__(self, ob):
self.__subject__ **= ob
return self
def __rpow__(self, ob):
return pow(ob, self.__subject__)
class Proxy(ProxyBase):
'''
A variable container for passing references.
Example 1:
```python
a = Proxy(1)
assert a == 1
```
Example 2:
```python
plus_1 = lambda x: x = x + 1
a = Proxy(1)
plus_1(a)
assert a == 2
```
'''
def __init__(self, val):
self.set(val)
def set(self, val):
self.__subject__ = val
class Delayed:
'''Used to store a function and all its arguments for later calls.'''
def __init__(self, func: Union[Callable, Delayed] = None):
if isinstance(func, Delayed):
self.func = func.func
self.args = func.args
self.kw = func.kw
else:
self.func = func
self.args = ()
self.kw = {}
def __call__(self, *args, **kw):
self.args = args
self.kw = kw
return self
def trigger(self, **kw):
if self:
return self.func(*self.args, **{**self.kw, **kw})
def __bool__(self):
return bool(self.func)
def __repr__(self):
return f"<{self.__class__.__name__} definition for {self.func.__name__} at {hex(id(self))}>"
class FactoryProxy(Delayed, ProxyBase):
'''
Stores a function representation. When the value is retrieved, the function will be executed
and its return value will be returned.
Example:
```python
l = [1]
listlen = FactoryProxy(lambda: len(l))
assert listlen == 1
l.append(1)
assert listlen == 2
```
'''
__noproxy__ = ("func", "args", "kw")
@property
def __subject__(self):
return super().trigger()
以上代码实现了四个类:ProxyBase,Proxy,Delayed,FactoryProxy。
ProxyBase 是代理模式的基础类,实现的功能是:任何访问实例的操作都等同于直接访问实例__subject__(包括 isinstance)。
Proxy 是最简单的代理类,相当于一个变量容器,可以使之等同于 set 到容器的任何变量,同时保持变量的可变性。
Delayed 是一个函数操作的定义类,存储了函数、args、kwargs,以待之后执行,可以通过以下代码定义:
def func(a, b=1):
print(a, b)
c = Delayed(func)(5, b=10)
FactoryProxy 是一个基于函数的代理类,与 Delayed 是相同的格式,区别在于实例函数直接等同于函数的结果。函数将在变量被取值时执行。
感谢阅读,如果本文对你有帮助,可以订阅本系列:代码怪兽的前端技术分享,我将继续分享前后端全栈开发的相关实用经验。祝你开发愉快。
公益项目 GithubStar.Pro 的后端也使用了这种模式,因此这篇文章是开发的实际经验,这里也推荐使用 GithubStar.Pro 推广您的项目~