背景
程序设计好之后,写的具体代码核心要满足可读性.方便未来的debug与修改功能.本规范试图把程序设计领域的经验智慧与团队的具体实际相结合.
for-if-else不能超过三层
Flat is better than nested. --The Zen of Python
- 其中第三层的if与else分支不允许超过2个。
可以通过增加elif,或者抽取出函数,或者分拆成多步来达到这个目标.
例如: 写一个很复杂的循环求td_head_list, col_name_list两个值, 可以变为写第一个循环求col_name_list, 第二个循环再求td_head_list - 不允许逻辑判断过于复杂(超过30个字符或含有超过两个逻辑运算符)
可以包装在函数里,利用is_somecase()这样的函数来说明含义.
def get_page_amount(self) {
if (is_dead()):
return self.dead_amount();
if (is_separated()):
return self.separated_amount();
return self.normal_pay_amount();
};
- if与else后面不允许紧跟着判断语句
if is_comment():
do_something()
else: # 改为 elif: is_code(): do_something2(); else: do_something3()
if is_code():
do_something2()
else
do_something3()
};
- 利用DataFrame这样的数据结构减少循环与判断,可以参考一下两段代码.
even_list = []
for i in int_list:
if i is None:
continue
elif i % 2 == 0:
even_list.append(i)
int_frm = pd.DataFrame(columns=['int'], data=int_list)
even_frm = int_frm[int_frm['int'] % 2 == 0]
- 对浮点数,不允许使用等于或者大于等于来判断大小.可以转化为整数之后再判断.
注释
- 必须要注释的地方:类声明,超过3行的方法。位置如下:
class SmartBoy()
"""初始化需要iq变量"""
def find_girl(self, place):
"""place需要时一个地址"""
- 项目中的注释行数大于等于代码行的50%。
- 注释推荐使用中文.内部的一些专有名词使用英文.例如
line_list 的成员是.py文件一行
- 代码修改之后要修改相应的注释.
- 一般避免在函数体类进行注释, 可以考虑采用变量命名方法让程序自解释.参考如下的重构方式
if a>b and c<2: #代表居中的情况
do_something()
if is_center():
do_something()
注释的写法可以参考pandas
以及:https://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_google.htm
try-except中的try语句不超过两行
-
try:
包裹的语句出现问题难以调试,因为可能掩盖了其中的bug。 - except 语句需要捕捉到的错误类型要注明。
print 规范
print必须包含name, 这样知道print是从哪里来的.
print('{}: start!'.format(__name__))
默认参数不能是可变对象
任何函数的默认参数都不可以是可变对象,以下默认参数的写法是错的.这样会导致python程序重复创建等问题.
def __init__(self, io_behavior=IoBehavior(),a_dict = dict()):
可以写为:
def __init__(self, io_bhv=None):
if io_bhv is None:
self.io_bhv= IoBhv()
或使用ensure_class 函数简化代码.
from helper import ensure_class # 从ylib.helper导入
# 如果a_var为None的话,会返回IoBhv(), 不为None的时候会检查io_bhv是否属于IoBhv类型.
self.frm = ensure_class(io_bhv, IoBhv)
判断语句
- 与None判断的时候需要用is, 要注意pandas元素中None与python中自带的None是有区别的, 使用
is None
会返回False - is是判断内存中指向同一个值,有时候会发生莫名其妙的错误.建议全部改用==
frm中的None
如果frm中某一列是正整数,需要使用yfrm.INT_NONE来表示None的含义.如果直接使用python自带的none会把整个一列都变成为浮点类型.
如果frm中某一列的有效值允许为负整数,要么确保所有的数都大于-999,要么直接转化为浮点数来表示.
for循环中,不允许改动迭代对象
for i in list1:
list1.pop() # 改动了list1,不允许.
如果本.py文件中存在中文字符,需要声明编码
#coding=utf-8
不允许出现重复3遍的代码组团
一旦发现这个确实是同一个逻辑重复了3遍.应该重构为一个方法或函数. 平常可以积累自己常用的重构方法.参考下面的重构方法:
py_codeline_dict[IMPORT_NUM] = len(import_lines)
py_codeline_dict[CLASS_NUM:class_lines] = len(class_lines)
# 后面还有好几行
cal_dict = {IMPORT_NUM: import_lines, CLASS_NUM:class_lines}
py_codeline_dict = {}
for key_i in cal_dict:
py_codeline_dict[key_i] = len(cal_dict[key_i])
所有的requests都要设置time_out参数.
避免程序卡死,也方便在超时的时候发现问题所在.一般为5秒.
使用的所有常量都要集中管理
- 不允许在代码中出现没有预先定义的常量
- 如果是这个类共享的常量:放在类名下面定
- 如果本文件夹中的类都要共享的常量:单独建立一个config.py文件,把常数放进去.
- 对于字符串的命名,一般为 YES ='yes' 这样的形式.不允许 YES ='y'.
一行代码不要过长
不要超过pycharm给的推荐限制. 此时需要分行。
每个方法的代码不允许超过15行
不包括assert以及raise语句。可以通过提取结构, 优化语句等方式实现。
命名规范
[数据命名]
- dict的元素含有list或dict的时候都使用_json来命名.
- list的元素含有dict: _dict_list,
[pickle文件后缀名]
- 父类一般使用Base来开头
- 数据类型与后缀的关系:
pandas.Series -> .srs
pandas.DataFrame -> .frm
dict -> .dict
list - > .list
string -> .str
set -> .set
orderedict -> .odict
defualtdict -> .ddict
tuple -> .tuple
[文件名,变量名与类名<16个字符]
- 超过之后可读性就会很差.可以考虑: 1. 运用英文缩写(把元音字母去掉), 例如 behavior -> bhvr, control -> ctrl, coodinator-> cdntr
- 把长的英文单词用的短的英文单词代替. 例如: get_page_source -> get_page_url, download_pdf -> fetch_pdf, frm_coordinator->frm_crdnt
[不允许出现高度相似的名称]
典型如只有一个字符的区别: divc_tag之于 divct_tag, sse_worker 之于 sze_worker
实在无法避免可以把字符重复两遍,例如div_tt_frm
与div_cc_frm
[定义好概念]
- 写项目之前,需要定义概念,然后遵循这些概念来定义变量名.
例如: 定义 file_line 为.py文件的每一行, code_line代表.py文件中的每一行代码.
基本类型的临时变量的值通常不允许变更
- 临时变量指的是只作用在本方法或函数中的变量。只有在for循环等有必要的地方才允许变更.
a = 89
b = a +3
a = 12 # 不想直接变更值,可以改为c = 12
- 数字,字符串,字符等都是基本类型。
warnings
避免代码在执行中因为使用了外部库不建议的用法而出现warnings
设计规范
[状态码]
- 所有的状态使用100~999的三位整数来表示
- 通用的规则(借鉴了HTTP状态码):
100:新建
200:成功
204:无需处理
400:出错。例如发现文件是坏的,要求重新下载。 4XX可以用来表示具体的错误原因。
404:文件未找到。例如发现给的file_path是没有找到文件)
类名规范
- 类名不建议使用Get开头.如果表示一个步骤使用Behv后缀.
- 利用设计模式的时候,建议使用behv的后缀来代表行为类. 利用cdnt后缀来表示中介者与外观模式中的中介者与外观, 相应的wrkr后缀表示模式中的细分逻辑类.
- 利用观察者模式的时候, 被观察者建议使用sgnl后缀表示, 观察者使用obs后缀表示.
类方法规则
-
__init__
>原子方法>分子方法 原子方法代表不依赖与本类任何的方法.分子方法代表会依赖于本类的属性或方法的方法. - 不允许在子类里面出现init函数
- 不允许在init以外的地方定义新的实例属性, 参考下面的重构方法.
class Duck:
def __init__(self, name):
self.name = name
def fly(self, wings):
self.wings = wings
class Duck:
def __init__(self, name, wings):
self.name = name
self.wings = wings
def fly(self):
print(self.wings)
- 子类重写父类的方法时,要保持相同的参数列表。
示例:下面代码中的RedDuck的fly方法与父类中的参数不一致。不允许。
class Duck:
def __init__(self, name):
self.name = name
def fly(self):
print(self.name)
class RedDuck(Duck):
def fly(self, height):
print(height)
- 父类中需要子类重写的方法必须有NotImplementError提示。
class Duck:
def fly(self):
raise NotImplementedError
class RedDuck:
def fly(self):
print("[RED_DUCK]I can fly!")
- 凡是会被其他类作为参数的类都必须有父类.
- 不希望外界调用的方法注明秘方. 除了本module以外的代码不允许调用秘方. 通常一个方法要么属于必须重写的方法(父类中是 NotImplementError),要么是秘方。
def get_apple(self):
"""[秘方] 获取苹果"""
- 类的
__init__
方法的参数不允许超过5个. 参考一下重构:
class Duck:
def __init__(self, where, time, height, type, weight, color):
# N行赋值语句
def fly(self):
# 利用实例属性完成飞行任务
class Duck:
def __init__(self, type, weight, color): #与这个类本性有关的参数放在这里,仅仅与单次行为有关的放在方法的参数里.
# N行赋值语句
def fly(self,where, time, height):
# 利用实例属性以及本次方法的参数完成飞行任务
- 另外的方案是实现一个工厂类,这个工厂类有一个create_instance方式,可以根据参数来创建不同的类实例.
日期参数
- 所有的日期参数使用180502这样的整形传递. 可以命名为date_int. 这样能够减少误用.
--------------------------------------2018-08-10 新增--------------------------------------
is_方法.
函数或方法如果返回的值为Ture or False, 那么以is作为前缀
do_方法
执行类的多步操作的方法要以do为前缀.
class Worker():
def action_1(self):
# some code
def action_2(self):
# some code
def do_job(self):
self.action_1()
self.action_2()
Bhv,Stp后缀,
行为类有Bhv后缀, 步骤类有Stp后缀.
Base前缀
- 父类一般有Base前缀,除非有更好的名字.
- 父类至少两个方法.可以把子类一些通用的代码放在父类里面.
常数定义
常数通常放在类定义下方定义,多个类要共享的常数放在config.py里面.
--------------------------------------2018-08-11 新增--------------------------------------
.py文件分隔
-原则上一个步骤类来与它依赖的行为类放在一个.py文件里面. 如果有多个步骤类有共同依赖的行为类,那么这个行为类可以单独拆分出来放在bhv.py文件里.
- 原则上一个步骤类对应tests文件夹中的一个test文件.
类的参数不可以为文件路径
- 除非类名中标注了IO(专门负责文件读写), 否则不允许使用文件路径作为参数.这样会引入了iostate这样的不必要的依赖了. iostate的依赖可以在测试用例中引入.参考下面的重构.
class md2line():
def get_code_lines.do(the_path)
res = io_state.read(the_path)# 引入了不必要的依赖,而且调用者无法直接传递res参数
def get_code_lines.do(the_frm)# 规范, 参数从路径变为数据
不允许在类里面新建其他类实例
- 除非是Fct(工厂类)后缀的类, 不允许在类的方法里面新建另外的类实例,只能作为参数传进来.参考下面的重构案例
class RedDuck:
def __init__(self):
self.fly_bhv = FlyBehavior()
def fly(self):
self.fly_bhv.fly()
class RedDuck:
def __init__(self, fly_behavior):
self.fly_behavior = fly_behavior
def fly(self):
self.fly_behavior.fly()
类型断言
Errors should never pass silently. -- The Zen of Python
- 函数的每个参数需要做类型断言.经常会需要使用如下代码:
assert isinstance(a_var, pd.DataFrame)
- 要求每个函数的返回值也要写assert来判断类型是否正确.如果有多个可能性的话,需要在各个分支上面分别检查类型.例如:
if deal_frm():
assert isinastance(res, pd.DataFrame)
return res
elif deal_list():
assert isinastance(res, list)
return res
else:
return 0 # 直接return一个常数的话不需要保证类型.
- 方法不允许既可能返回None,又可以返回其他类型的值.参考下面的重构案例:
def read(file_path):
if os.path.exists(file_path): # 不满足的话会返回None
return self.read_pickle(file_path)
def read(file_path):
if os.path.exists(file_path): # 不满足的话会直接报错
return self.read_pickle(file_path)
raise ValueError
__init__方法
自定义实例变量不应该超过2个.
如果发现自己代码中init方法中的实例变量超过两个,那么可以采用某个中间类来封装这些类变量.
def __init__(HtmlIo, IoBhv, FtpBhv) # 需要重构,可以使用状态模式封装,或者用工厂模式封装类的创建.
2018-08-13新增
引入第三方依赖
所有的第三方依赖库都要从yfack中引入(除了pandas).确保大家的版本都能够统一. 也能够节省大家写import的时间.范例:
from ypack import np, es, requests
注释里面要写数据结构的样式
[类别标志]
- 开发中经常需要用到分类。如果分类超过了3个,分类需要用类别码(整数)来标志。例如:
BLOCK_CMNT = 20
INLINE_CMNT = 21
CODE_LINE = 30
- 杜绝直接使用字符串或整数,需要定义成常量集中管理, 可以参考如下的代码重构
if is_block_comment():
the_type = 'block_comment' #这些常数散落在程序各处,未来很难以管理
elif is_inline_comment():
the_type = 'inline_comment'
INLINE_COMMNET = 'inline_comment' #这些常数通常集中在一起
BLOCK_COMMNET = 'block_comment'
# other codes...
if is_block_comment():
the_type = BLOCK_COMMNET
elif is_inline_comment():
the_type = INLINE_COMMNET
2018-08-17新增
- 每个类至少有2个或以上的方法(继承的方法不算,含有
__init__
方法).
如果类过小,可以考虑与其他的类合并或者把父类某些参数做成可配置的.参考下面的重构:
class BaseSpider():
HEADERS = "abc"
// other codes
class SHSpider():
HEADERS = "bcd"
class BaseSpider():
HEADERS = "abc"
def change_paras(headers):
self.HEADERS = headers #这样就不需要SHSpider类了.
2018-12-25新增
【datadesign规范】
1.每次项目之前都要设计数据结构。需要有一个data_design.py文件放上所有input与output数据的schema。
2.参考ylib\helper\tests\test_yassert.py中的方式判定数据结构。dataframe的数据格式判定可以通过把它转化为dict之后再判定(或者直接自己写helper函数)
2.data_design.py有任何变动,需要在早会与下午会中讨论。