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

Clean code in Python

参考来源: 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))

DRY principle: Don’t Repeat Yourself!

不惜一切代价避免重复代码!

建议的解决方案:decorators(装饰器)

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_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 -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_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 = 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 details2.操作属于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

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 = 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)

3. Context Managers (上下文管理)

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断开等。
    • 如果代码段成功结束,那么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...]) 该方法允许类的实例跟函数一样表现

你可能感兴趣的:(python,python,编程)