python项目

始于2020 05~

Posted by Tao on May 31, 2020

平时使用的时候,常常只是一次性的脚本。但是在构建稍大的项目的时候,需要一些设计方法。

总结一些平时觉得比较好的设计模式。

参考项目及网站

项目标准

  • 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