【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

构造和初始化
methoddescriptiondescription
__new__构造函数创建类并返回这个类的实例(很少用)
__init__构造函数将传入的参数来初始化该实例
__del__析构函数当一个对象进行垃圾回收时候的行为
属性访问控制
methoddescriptiondescription
__getattr__(self, name)定义访问一个不存在的属性时的行为只有该属性不存在时才会起作用
__setattr__(self, name, value)定义对属性进行赋值和修改操作时的行为要避免"无限递归"的错误
__delattr__(self, name)定义删除属性时的行为要避免"无限递归"的错误
__getattribute__(self, name)定义了属性被访问时的行为要避免"无限递归"的错误;最好不要尝试去实现,很少这么做的
描述器对象

描述符?

描述器对象不能独立存在, 它需要被另一个所有者类所持有。

描述器对象可以访问到其拥有者实例的属性。

在面向对象编程时,如果一个类的属性有相互依赖的关系时,使用描述器来编写代码可以很巧妙的组织逻辑。

一个类要成为描述器,必须实现__get__, __set__, __delete__ 中的至少一个方法。

下表中:参数instance是拥有者类的实例。参数owner是拥有者类本身

methoddescription
__get__(self, instance, owner)在其拥有者对其读值的时候调用
__set__(self, instance, value)在其拥有者对其进行修改值的时候调用
__delete__(self, instance)在其拥有者对其进行删除的时候调用
构造自定义容器(Container)

在Python中,常见的

  • 不可变容器:tuple, string
  • 可变容器:dict, list

如果我们要自定义一些数据结构,使之能够跟以上的容器类型表现一样,那就需要去实现某些协议:

  • 自定义不可变容器类型,需定义:__len____getitem__方法;
  • 自定义可变容器类型,需定义:__len____getitem____setitem____delitem__
  • 如果你希望自定义数据结构还支持"可迭代", 那就还需要定义__iter__
methoddescription
__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在容器中找不到时触发的行为。
上下文管理
对象的序列化
运算符相关的:
  • 比较运算符
  • 一元运算符和函数
  • 算术运算符
  • 反算术运算符
  • 增量赋值
  • 类型转换
其它魔术方法
methoddescription
__str__(self)对实例使用str()时调用
__repr__(self)对实例使用repr()时调用。

str()repr()都是返回一个代表该实例的字符串,
主要区别在于: str()的返回值要方便人来看,而repr()的返回值要方便计算机看。

methoddescription
__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...])该方法允许类的实例跟函数一样表现
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Apress 2019出版 Python is one of the most popular languages today. Relatively new fields such as data science, AI, robotics, and data analytics, along with traditional professions such as web development and scientific research, are embracing Python. It’s increasingly important for programmers writing code in a dynamic language like Python to make sure that the code is highquality and error-free. As a Python developer, you want to make sure that the software you are building makes your users happy without going over budget or never releasing. Python is a simple language, yet it’s difficult to write great code because there aren’t many resources that teach how to write better Python code. Currently lacking in the Python world are code consistency, patterns, and an understanding of good Pythonic code among developers. For every Python programmer, great Pythonic code has a different meaning. The reason for this could be that Python is being used in so many areas that it’s difficult to reach consensus among developers about specific patterns. In addition, Python doesn’t have any books about clean code like Java and Ruby do. There have been attempts to write those kinds of books to bring clarity to good Python practices, but those attempts have been few and far between, and quickly frankly, they haven’t been high-quality. The main goal of this book is to provide tips to Python developers of various levels so they can write better Python software and programs. This book gives you various techniques irrespective of the field you use Python in. This book covers all levels of Python, from basic to advanced, and shows you how to make your code more Pythonic. Remember, writing software is not only science but art, and this book will teach you how to become a better Python programmer.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值