平时使用的时候,常常只是一次性的脚本。但是在构建稍大的项目的时候,需要一些设计方法。
总结一些平时觉得比较好的设计模式。
参考项目及网站
项目标准
- flake8 语法检查工具
- isort 对import顺序检测修复
- tox virtualenv 管理器和命令行测试工具
- Makefile 快速执行命令
- pytest 单元测试
'''
standard/
├── Makefile
├── src
│ ├── __init__.py
│ └── simple.py
├── tests
│ ├── hello_test.py
│ └── __init__.py
└── tox.ini
'''
# Makefile
SHELL = bash
all: isort isort_check lint test
isort:
@isort -s venv -s venv_py -s .tox -rc --atomic .
isort_check:
@isort -rc -s venv -s venv_py -s .tox -c .
lint:
@flake8
test:
@tox
clean:
@rm -rf .pytest_cache .tox bytedmypackage.egg-info
@rm -rf tests/*.pyc tests/__pycache__
.IGNORE: install_dev
.PHONY: all check isort isort_check lint test
注册类
类工厂模式。一个文件夹视作一个大类,内部包含一系列类似的类,在__init__.py文件中进行注册,通过python装饰器语法将被装饰的类注册到一个字典中,然后通过大类暴露出的统一的build函数,传入名字字符串参数,初始化不同的类。
这样做的好处是在新加入一个类的时候,只需要增加当前的文件,不需要修改其他的地方的代码。注册,导入都可以自动实现。
'''
文件目录
.
├── registry.py
└── text
├── __init__.py
└── space_tokenizer.py
'''
# registry.py
REGISTRIES = {}
def setup_registry(
registry_name: str,
base_class=None,
default=None
):
REGISTRY = {}
REGISTRY_CLASS_NAMES = set()
REGISTRIES[registry_name] = {
'registry': REGISTRY,
'default': default,
}
def build_x(args, *extra_args, **extra_kwargs):
choice = getattr(args, registry_name, None)
print('build_x:', choice, args, REGISTRY)
if choice is None:
return None
cls = REGISTRY[choice]
if hasattr(cls, 'build_' + registry_name):
builder = getattr(cls, 'build_x' + registry_name)
else:
builder = cls
return builder(*extra_args, **extra_kwargs)
def register_x(name):
def register_x_cls(cls):
if name in REGISTRY:
raise ValueError('duplicate!')
if cls.__name__ in REGISTRY_CLASS_NAMES:
raise ValueError('duplicate class name!')
if base_class is not None and not issubclass(cls, base_class):
raise ValueError('must extend!')
REGISTRY[name] = cls
REGISTRY_CLASS_NAMES.add(cls.__name__)
print('register_x', REGISTRY, REGISTRY_CLASS_NAMES)
return cls
return register_x_cls
return build_x, register_x, REGISTRY
# text/__init__.py
import os
import importlib
import registry
build_tokenizer, register_tokenizer, TOKENIZER_REGISTRY = registry.setup_registry(
'tokenizer',
default=None
)
for file in os.listdir(os.path.dirname(__file__)):
if file.endswith('.py') and not file.startswith('_'):
module = file[:file.find('.py')]
importlib.import_module('text.' + module)
# text/space_tokenizer.py
import re
from text import register_tokenizer
@register_tokenizer('space')
class SpaceTokenizer(object):
def __init__(self):
self.space_tok = re.compile(r'\s+')
def encode(self, x: str) -> str:
return self.space_tok.sub(' ', x)
def decode(self, x: str) -> str:
return x
抽象基类
使用抽象基类的作用类似于JAVA中的接口。在接口中定义各种方法,然后继承接口的各种类进行具体方法的实现。抽象基类就是定义各种方法而不做具体实现的类,任何继承自抽象基类的类必须实现这些方法,否则无法实例化。
比如下面的例子中,继承了Phone这个抽象基类的Apple必须实现cname和change_name两个方法,不然不能初始化。
from abc import ABCMeta, abstractmethod
import six
@six.add_metaclass(ABCMeta)
class Phone(object):
def __init__(self, name):
self.name = name
@property
@abstractmethod
def cname(slef) -> str:
return self.name
@abstractmethod
def change_name(self, name):
raise NotImplementedError
class PhoneNew(object):
def __init__(self, name):
self.name = name
def change_name(self, name):
self.name = name
class Apple(Phone):
def __init__(self, name):
super(Apple, self).__init__(name)
@property
def cname(self) -> str:
return self.name + '_Apple'
def change_name(self, name):
self.name = name