参考来源: https://ep2016.europython.eu/media/conference/slides/clean-code-in-python.pdf
版本一:Meaning
其中 if
语句是用来判断是否为闰年(很长)
def elapse(year):
days = 365
if year % 4 == 0 or (year % 100 == 0 and year % 400 ==0):
days += 1
for 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 = 365
if is_leap(year):
days += 1
for day in range(1, days+1):
print("Day {} of {}".format(day, year))
不惜一切代价避免重复代码!
建议的解决方案:decorators(装饰器)
总体思路: 定义一个函数并对其进行修改,然后返回具有更改后逻辑的新函数。
def decorator(original_function):
def inner(*args, **kwargs):
# modify original function, or add extra logic
return 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 -1
else:
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 orders
WHERE 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 -1
else:
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 -1
else:
logger.info("%s run successfully", function_name)
return 0
return 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 orders
WHERE 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)
get_*()
, set_*()
(注:本小节以下内容来自:一篇文章搞懂Python装饰器所有用法(建议收藏))
property
是 python 内置的一个装饰器。通常存在于类中,可以将一个函数定义成一个属性,属性的值就是该函数return的内容。
通常我们给实例绑定属性是这样的:
class Student(object):
def __init__(self, name, age=None):
self.name = name
self.age = age
#实例化:
XiaoMing = Student("小明")
#添加属性
XiaoMing.age = 25
#查询属性
XiaoMing.age
#删除属性
del XiaoMing.age
#再次查看就没了
XiaoMing.age
但是这样直接吧属性暴露出去,虽然写起来简单,但是并不能对属性的值做合法性限制。为了实现这个功能,我们可以这样写:
class Student(object):
def __init__(self, name):
self.name = name
def set_age(self, age):
if not isinstance(age, int):
raise ValueError('输入不合法:年龄必须为数值!')
if not 0 < age < 100:
raise ValueError('输入不合法:年龄范围必须为0-100')
self._age = age
def get_age(self):
return self._age
def 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
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise ValueError('输入不合法:年龄必须为数值!')
if not 0 < value < 100:
raise ValueError('输入年龄不合法:年龄范围必须为0-100')
self._age = value
@age.deleter
def age(self):
del self._age
XiaoMing = 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 = key
self._points = 0
def set_points(self, value):
self._points = value
def get_points(self):
return self._points
def 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 = key
self._points = 0
@property
def points(self):
return self._points
@points.setter
def points(self, new_points):
self._points += new_points
#实例化
XiaoMing = PlayerStatus(123)
print(XiaoMing.points) # 0
XiaoMing.points = 20
print(XiaoMing.points) # 20
XiaoMing.points += 30
print(XiaoMing.points) # 50
在 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 = False
for category in current_stock.categories:
for prod in category.products:
if prod.count > 0 and prod.if == product.id:
product_available_in_stock = True
if 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)
class DBHandler:
def __enter__(self):
start_database_service()
return self
def __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断开等。
参考(很全,强烈推荐):介绍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) |
在其拥有者对其进行删除的时候调用 |
在Python中,常见的
如果我们要自定义一些数据结构,使之能够跟以上的容器类型表现一样,那就需要去实现某些协议:
__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...]) |
该方法允许类的实例跟函数一样表现 |