Clean code in Python
参考来源: https://ep2016.europython.eu/media/conference/slides/clean-code-in-python.pdf
逻辑分离,每个函数只做好一件事
版本一:Meaning
其中 if
语句是用来判断是否为闰年(很长)
def elapse(year):days = 365if year % 4 == 0 or (year % 100 == 0 and year % 400 ==0):days += 1for day in range(1, days+1):print("Day {} of {}".format(day, year))#elapse(2019)
版本二:Meaning and logic separation
将 if
判断语句单独分离出来
是不是清爽了很多
def is_leap(year):return year % 4 == 0 or (year % 100 == 0 and year % 400 ==0)def elapse(year):days = 365if is_leap(year):days += 1for day in range(1, days+1):print("Day {} of {}".format(day, year))
DRY principle: Don’t Repeat Yourself!
不惜一切代价避免重复代码!
建议的解决方案:decorators(装饰器)
decorators
总体思路: 定义一个函数并对其进行修改,然后返回具有更改后逻辑的新函数。
def decorator(original_function):def inner(*args, **kwargs):# modify original function, or add extra logicreturn original_function(*args, **kwargs)return inner
举个例子:
假设现在有一个 update_db_indexes
函数,先尝试执行commands
,执行成功返回0, 执行失败返回-1
def update_db_indexes(cursor):commands = ("""REINDEX DATABASE transactional""",)try:for command in commands:cursor.execute(command)except Exception as e:logger.exception("Error in update_db_indexes: %s", e)return -1else:logger.info("update_db_indexes run successfully")return 0
有另外一个 move_data_archives
函数, 先尝试执行commands
,执行成功返回0, 执行失败返回-1
def move_data_archives(cursor):commands = ("""INSERT INTO archive_orders SELECT * from ordersWHERE order_date < '2016-01-01' ""","""DELETE form orders WHERE order_date < '2016-01-01'""", )try:for command in commands:cursor.execute(command)except Exception as e:logger.exception("Error in move_data_archives: %s", e)return -1else:logger.info("move_data_archives run successfully")return 0
上述两个函数的逻辑是一样的,代码存在大段的重复。
所以将其公共的部分抽象出来,先定义一个 db_status_handler
函数,作为装饰器。
这个装饰器装饰的是 db_script_function
函数,
装饰器内函数所做的事情是:执行 db_script_function
的 commands
,执行成功返回0, 执行失败返回-1
def db_status_handler(db_script_function):def inner(cursor):commands = db_script_function(cursor)function_name = db_script_function.__qualname__try:for command in commands:cursor.execute(command)except Exception as e:logger.exception("Error in %s: %s", function_name, e)return -1else:logger.info("%s run successfully", function_name)return 0return inner
现在对于前面的 update_db_indexes
函数 和 move_data_archives
函数 就可以精简为:
@db_status_handler
def update_db_indexes(cursor):return ("""REINDEX DATABASE transactional""",)@db_status_handler
def move_data_archives(cursor):return ("""INSERT INTO archive_orders SELECT * from ordersWHERE order_date < '2016-01-01' ""","""DELETE from orders WHERE order_date < '2016-01-01'""",)
update_db_indexes
被 db_status_handler
装饰,相当于 db_status_handler(update_db_indexes)
move_data_archives
被 db_status_handler
装饰,相当于 db_status_handler(move_data_archives)
Implementation details
- Abstract implementation details
- Separate them from business logic
- We could use:
- Properties
- Magic methods
- Context managers
1. @property
- Compute values for objects, based on other attributes
- Avoid writing methods like
get_*()
,set_*()
- Use Python’s syntax instead
(注:本小节以下内容来自:?一篇文章搞懂Python装饰器所有用法(建议收藏))
property
是 python 内置的一个装饰器。通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。
通常我们给实例绑定属性是这样的:
class Student(object):def __init__(self, name, age=None):self.name = nameself.age = age#实例化:
XiaoMing = Student("小明")#添加属性
XiaoMing.age = 25#查询属性
XiaoMing.age#删除属性
del XiaoMing.age#再次查看就没了
XiaoMing.age
但是这样直接吧属性暴露出去,虽然写起来简单,但是并不能对属性的值做合法性限制。为了实现这个功能,我们可以这样写:
class Student(object):def __init__(self, name):self.name = namedef set_age(self, age):if not isinstance(age, int):raise ValueError('输入不合法:年龄必须为数值!')if not 0 < age < 100:raise ValueError('输入不合法:年龄范围必须为0-100')self._age = agedef get_age(self):return self._agedef del_age(self):self._age = None#实例化:
XiaoMing = Student("小明")#添加属性
XiaoMing.set_age(25)#查询属性
XiaoMing.get_age()#删除属性
XiaoMing.del_age()#再次查看
XiaoMing.get_age()
上面的代码设计虽然可以约束变量的取值,但是发现不管是获取还是赋值(通过函数)都和我们平时见到的不一样。
按照我们的思维习惯应该是这样的:
# 赋值
XiaoMing.age = 25# 获取
XiaoMing.age
也就是说,我们要尽量避免使用类似 get_*()
, set_*()
的方法,而使用 Python 正常的语法习惯。
这样的方式我们如何实现呢?请看下面的代码:
class Student(object):def __init__(self, name):self.name = name@propertydef age(self):return self._age@age.setterdef age(self, value):if not isinstance(value, int):raise ValueError('输入不合法:年龄必须为数值!')if not 0 < value < 100:raise ValueError('输入年龄不合法:年龄范围必须为0-100')self._age = value@age.deleterdef age(self):del self._ageXiaoMing = Student("小明")#设置属性
XiaoMing.age = 25#查询属性
XiaoMing.age#删除属性
del XiaoMing.age#再次查询
XiaoMing.age
用@property
装饰过的函数,会将一个函数定义成一个属性,属性的值就是该函数return的内容。同时,会将这个函数变成另外一个装饰器。就像后面我们使用的@age.setter 和 @age.deleter
。
-
@age.setter
使得我们可以使用XiaoMing.age = 25
这样的方式直接赋值。 -
@age.deleter
使得我们可以使用del XiaoMing.age
这样的方式来删除属性。
(注:本小节以上内容来自:?一篇文章搞懂Python装饰器所有用法(建议收藏))
再举个例子:
假设现在有个“吃豆豆”游戏,用类 PlayerStatus
表示,实例化时 key
表示玩家的 id。
该类的属性 points
表示该玩家目前吃了多少个豆豆
初始化时 points
设置为0
当吃到新的豆豆时,则更新 points
- 以下实现方式通过
set_points()
函数来设置points
,用get_points
来获取points
,不符合python的语法习惯:
class PlayerStatus:def __init__(self, key):self.key = keyself._points = 0def set_points(self, value):self._points = valuedef get_points(self):return self._pointsdef accumulate_points(self, new_points):# 1.读取current_score = self.get_points()# 2.操作score = current_score + new_points# 3.修改self.set_points(score)return#实例化
XiaoMing = PlayerStatus(123)
XiaoMing.accumulate_points(10)
XiaoMing.get_points()
其中 1.读取
和 3.修改
属于 implementation details,2.操作
属于business logic
应该将其分离开来
- 以下实现方法使用了内置装饰器
@property
, 对于属性points
可以同python里的普通变量一样用=
进行赋值,用+=
进行修改:
class PlayerStatus:def __init__(self, key):self.key = keyself._points = 0@propertydef points(self):return self._points@points.setterdef points(self, new_points):self._points += new_points#实例化
XiaoMing = PlayerStatus(123)print(XiaoMing.points) # 0XiaoMing.points = 20
print(XiaoMing.points) # 20XiaoMing.points += 30
print(XiaoMing.points) # 50
2. Magic methods(魔法方法)
在 Python 中,所有以__
双下划线包起来的方法,都统称为"魔术方法"。我们接触最多的是__init__
。
其实每个魔法方法都是在对内建方法的重写,做和像装饰器一样的行为。
举个例子:
class Stock:def __init__(self, categories=None):self.categories = categories or []self._products_by_category = {
}def request_product_for_customer(customer, product, current_stock):#--------------------------------------------------------product_available_in_stock = Falsefor category in current_stock.categories:for prod in category.products:if prod.count > 0 and prod.if == product.id:product_available_in_stock = Trueif product_available_in_stock:#--------------------------------------------------------requested_product = current_stock.request(product)customer.assign_product(requested_product)else:return "Product not available"
将上述代码虚线框部分在做的事情是:查找product是否存在于current_stock中,并且当前库存大于0(Looking for elements).
这部分可以抽象出来,用一句代码来实现:
class Stock:def __init__(self, categories=None):self.categories = categories or []self._products_by_category = {
}def request_product_for_customer(customer, product, current_stock):#--------------------------------------------------------if product in current_stock:#--------------------------------------------------------requested_product = current_stock.request(product)customer.assign_product(requested_product)else:return "Product not available"
一个类要能执行 item in ...
,必须定义:
__contains__(self, item)
方法,让它变成一个容器(container)。
也就是说,如果定义了该方法,那么在执行item in container
或者item not in container
时该方法就会被调用。
(如果没有定义,那么Python会迭代容器中的元素来一个一个比较,从而决定返回True或者False。)
class Stock:def __init__(self, categories=None):self.categories = categories or []self._products_by_category = {
}def request_product_for_customer(customer, product, current_stock):#--------------------------------------------------------if product in current_stock:#--------------------------------------------------------requested_product = current_stock.request(product)customer.assign_product(requested_product)else:return "Product not available"def __contains__(self, product):self._products_by_category()available = self.categories.get(product.category)
3. Context Managers (上下文管理)
class DBHandler:def __enter__(self):start_database_service()return selfdef __exit__(self, *exc):stop_databaset_service()with DBHandler():run_offline_db_backup()
在with
声明的代码段中,我们可以做一些对象的开始操作和清除操作,还能对异常进行处理。
这需要实现两个魔术方法: __enter__
和__exit__
。
__enter__(self)
: 可以定义代码段开始的一些操作。__exit__(self, exception_type, exception_value, traceback)
: 代码段结束后的一些操作,可以这里执行一些清除操作,或者做一些代码段结束后需要立即执行的命令,比如文件的关闭,socket断开等。- 如果代码段成功结束,那么exception_type, exception_value, traceback 三个参数传进来时都将为None。
- 如果代码段抛出异常,那么传进来的三个参数将分别为: 异常的类型,异常的值,异常的追踪栈。
魔法方法补充:
?参考(很全,强烈推荐):介绍Python的魔术方法 - Magic Method
构造和初始化
method | description | description |
---|---|---|
__new__ |
构造函数 | 创建类并返回这个类的实例(很少用) |
__init__ |
构造函数 | 将传入的参数来初始化该实例 |
__del__ |
析构函数 | 当一个对象进行垃圾回收时候的行为 |
属性访问控制
method | description | description |
---|---|---|
__getattr__(self, name) |
定义访问一个不存在的属性时的行为 | 只有该属性不存在时才会起作用 |
__setattr__(self, name, value) |
定义对属性进行赋值和修改操作时的行为 | 要避免"无限递归"的错误 |
__delattr__(self, name) |
定义删除属性时的行为 | 要避免"无限递归"的错误 |
__getattribute__(self, name) |
定义了属性被访问时的行为 | 要避免"无限递归"的错误;最好不要尝试去实现,很少这么做的 |
描述器对象
描述符?
描述器对象不能独立存在, 它需要被另一个所有者类所持有。
描述器对象可以访问到其拥有者实例的属性。
在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。
一个类要成为描述器,必须实现__get__
, __set__
, __delete__
中的至少一个方法。
下表中:参数instance
是拥有者类的实例。参数owner
是拥有者类本身
method | description |
---|---|
__get__(self, instance, owner) |
在其拥有者对其读值的时候调用 |
__set__(self, instance, value) |
在其拥有者对其进行修改值的时候调用 |
__delete__(self, instance) |
在其拥有者对其进行删除的时候调用 |
构造自定义容器(Container)
在Python中,常见的
- 不可变容器:tuple, string
- 可变容器:dict, list
如果我们要自定义一些数据结构,使之能够跟以上的容器类型表现一样,那就需要去实现某些协议:
- 自定义不可变容器类型,需定义:
__len__
和__getitem__
方法; - 自定义可变容器类型,需定义:
__len__
,__getitem__
,__setitem__
和__delitem__
。 - 如果你希望自定义数据结构还支持"可迭代", 那就还需要定义
__iter__
。
method | description |
---|---|
__len__(self) |
需要返回数值类型,以表示容器的长度 |
__getitem__(self, key) |
执行self[key] 时调用.调用的时候,如果key的类型错误,该方法应该抛出TypeError;如果没法返回key对应的数值时,该方法应该抛出ValueError。 |
__setitem__(self, key, value) |
执行self[key] = value 时调用 |
__delitem__(self, key) |
执行del self[key] 时调用 |
__iter__(self) |
需要返回一个迭代器(iterator)。执行for x in container: 或使用iter(container) 时被调用。 |
__reversed__(self) |
执行内建函数reversed() 时调用 |
__contains__(self, item) |
执行item in container 或 item not in container 时被调用 |
__missing__(self, key) |
dict字典类型有该方法,定义了key在容器中找不到时触发的行为。 |
上下文管理
对象的序列化
运算符相关的:
- 比较运算符
- 一元运算符和函数
- 算术运算符
- 反算术运算符
- 增量赋值
- 类型转换
其它魔术方法
method | description |
---|---|
__str__(self) |
对实例使用str()时调用 |
__repr__(self) |
对实例使用repr()时调用。 |
str()
和repr()
都是返回一个代表该实例的字符串,
主要区别在于: str()
的返回值要方便人来看,而repr()
的返回值要方便计算机看。
method | description |
---|---|
__format__(self, formatstr) |
在需要格式化展示对象的时候非常有用,比如格式化时间对象。 |
__hash__(self) |
对实例使用hash()时调用, 返回值是数值类型。 |
__bool__(self) |
对实例使用bool()时调用, 返回True或者False。 |
__dir__(self) |
对实例使用dir()时调用。通常实现该方法是没必要的 |
__sizeof__(self) |
对实例使用sys.getsizeof()时调用。返回对象的大小,单位是bytes |
__instancecheck__(self, instance) |
对实例调用isinstance(instance, class)时调用。 返回值是布尔值。它会判断instance是否是该类的实例 |
__subclasscheck__(self, subclass) |
对实例使用issubclass(subclass, class)时调用。返回值是布尔值。它会判断subclass否是该类的子类 |
__copy__(self) |
对实例使用copy.copy()时调用。返回"浅复制"的对象。 |
__deepcopy__(self, memodict={}) |
对实例使用copy.deepcopy()时调用。返回"深复制"的对象。 |
__call__(self, [args...]) |
该方法允许类的实例跟函数一样表现 |