当前位置: 代码迷 >> 综合 >> 【Clean Code】 代码简洁之道 之 Python
  详细解决方案

【Clean Code】 代码简洁之道 之 Python

热度:63   发布时间:2023-11-27 04:02:39.0

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_functioncommands,执行成功返回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_indexesdb_status_handler 装饰,相当于 db_status_handler(update_db_indexes)

move_data_archivesdb_status_handler 装饰,相当于 db_status_handler(move_data_archives)

Implementation details

  • Abstract implementation details
  • Separate them from business logic
  • We could use:
    1. Properties
    2. Magic methods
    3. 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 details2.操作属于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 containeritem 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...]) 该方法允许类的实例跟函数一样表现
  相关解决方案