4

让Python更优雅更易读(第一集) - yetangjian

 2 years ago
source link: https://www.cnblogs.com/yetangjian/p/16557118.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

让Python更优雅更易读(第一集)

变量和注释

在编写变量尽量要让其清晰只给,让人清除搞清楚代码的意图

下方两段代码作用完全一样,但第二段代码是不是更容易让人理解

value = s.strip()

username = input_string.strip()

 1.1变量的基础知识

1.1.1变量的交换

作为一门动态语言,我们不仅可以无需预先声明变量类型直接赋值

同时还可以在一行内操作多个变量,比如交换

parent, child = "father", "son"
parent, child = child, parent
print(parent)#son
 1.1.2变量解包
fruit = ["red apple","green apple","orange"]
*apple, orange =fruit
print(apple) #['red apple', 'green apple']
1.1.3变量用单下划线命名

它常作为一个无意义的占位符出现在赋值语句中,例如你想在解包时候忽略某些变量

fruit = ["red apple","green apple","orange"]
*_, orange =fruit #这里的_就意味着这个变量后续不太会使用的,可以忽略的
print(orange) #orange
1.1.4给变量注明类型

虽然Pyhton无需声明变量,声明了变量也无法起到校验的作用

但我们还是需要去注明类型来提高我们代码的可读性

最常见的做法是写函数文档,把参数类型与说明写在函数文档内

def hello_world(items):
    """
    这是进入编程大门的入口
    :param items: 待入门的对象
    :type items: 包含字符串的列表,[string,...]
    """
    pass

或者添加类型注解

from typing import List

def hello_world(items: List[str]):
    """
    这是进入编程大门的入口
    """

2.数值和字符串

2.1数值

2.1.1浮点数的精度问题
print(0.1+0.2)#0.30000000000000004
为了解决上述的精度问题我们可以用decimal这个内置函数
from decimal import Decimal
print(Decimal('0.1')+Decimal('0.2'))#必须用字符串表示数字

2.2字符串

2.2.1拼接多个字符串

最常见的做法是创建一个空列表,然后把需要拼接的字符串都放进列表中

最后使用str.join来拼接

除此之外也可以通过+号来拼接

3容器类型

最常见的内置容器类型有四种:列表、元组、字典、集合

3.1具名元祖

元组经常用来存放结构化数据,但只能通过数字来访问元组成员其实特别不方便

  1. 初始化具名函数
  2. 可以通过数字索引来访问
  3. 也可以通过名称来访问
from collections import  namedtuple
FruitColor = namedtuple('FruitColor','apple,orange')
fruitColor = FruitColor("RED","GREEN")
print(fruitColor[0])#RED
print(fruitColor.apple)#RED

Python3.6以后我们还可以用类型注解语法和typing.NameTuple来定义具名函数

from typing import NamedTuple
class FruitColor(NamedTuple):
    apple: str
    orange: str

fruitColor = FruitColor("RED","GREEN")
print(fruitColor[0])
print(fruitColor.apple)

3.2访问不存在的字典键

当用不存在的键访问字典内容时,程序会抛出KeyError异常

通常的做法:读取内容前先做一次条件判断,只有判断通过的情况下才继续执行其他操作

if "first" in example:
    num = example["first"]
else:
    num = 0
try:
    num = example["first"]
except KeyError:
    num = 0

如果只是“提供默认值的读取操作”,其实可以直接使用字典的.get()方法

#dict.get(key, default)方法接收一个default参数
example.get("first",0)

3.3使用setdefault取值并修改

比如我们有一个字典,这个字典内我们不知道有没有这个键

example = {"first":[1],"second":[2]}
try: example["third"].append(3) except KeyError: example["third"] = [3]

除了上述写法还有一个更合适的写法

调用dict.setdefault(key, default)会产生两种结果:

当key不存在时,该方法会把default值写入字典的key位置,并返回该值;

假如key已经存在,该方法就会直接返回它在字典中的对应值

example.setdefault("third",[]).append(3)

3.4认识字典的有序性

在Python 3.6版本以前,几乎所有开发者都遵从一条常识:“Python的字典是无序的。

”这里的无序指的是:当你按照某种顺序把内容存进字典后,就永远没法按照原顺序把它取出来了。

这种无序现象,是由字典的底层实现所决定的

Python里的字典在底层使用了哈希表(hash table)数据结构。当你往字典里存放一对key: value时,Python会先通过哈希算法计算出key的哈希值——一个整型数字;然后根据这个哈希值,决定数据在表里的具体位置

因此,最初的内容插入顺序,在这个哈希过程中被自然丢掉了,字典里的内容顺序变得仅与哈希值相关,与写入顺序无关

字典变为有序只是作为3.6版本的“隐藏特性”存在。但到了3.7版本,它已经彻底成了语言规范的一部分

4.条件分支

4.1分支基础注意事项

4.1.1不要显式地和布尔值做比较
#不推荐的写法
if example.is_deleted() == True:

# 推荐写法
if example.is_deleted():
4.1.2省略0值判断

在if分支里时,解释器会主动对它进行“真值测试”,也就是调用bool()函数获取它的布尔值

if containers_count == 0:
    pass
if fruits_list != []:
    pass

所以我们可以把代码改成如下:

if not containers_count:
    pass
if fruits_list:
    pass
4.1.3三元表达式

一种浓缩版的条件分支——三元表达式

#语法:
# true_value if <expression> else false_value
4.1.4修改对象的布尔值
from typing import List
class Length:
    def __init__(self,items: List[str]):
        self.items = items

lengthList = Length(["2","3"])

if len(lengthList.items) > 0 :
    pass

只要给UserCollection类实现__len__魔法方法,实际上就是为它实现了Python世界的长度协议

from typing import List
class Length:
    def __init__(self,items: List[str]):
        self.items = items

    def __len__(self):
        return len(self.items)

或者可以在类中实现__bool__

from typing import List
class Length:
    def __init__(self,items: List[str]):
        self.items = items

    def __bool__(self):
        return len(self.items)>2

lengthList = Length(["2","3"])

print(bool(lengthList)) #Fales

假如一个类同时定义了__len__和__bool__两个方法,解释器会优先使用__bool__方法的执行结果

 4.2优化分支代码

4.2.1优化枚举代码
class Movie:
    """电影对象数据类"""
    @property
    def rank(self):
        """按照评分对电影分级:

        - S: 8.5 分及以上
        - A:8 ~ 8.5 分
        - B:7 ~ 8 分
        - C:6 ~ 7 分
        - D:6 分以下
        """
        rating_num = float(self.rating)
        if rating_num >= 8.5:
            return 'S'
        elif rating_num >= 8:
            return 'A'
        elif rating_num >= 7:
            return 'B'
        elif rating_num >= 6:
            return 'C'
        else:
            return 'D'

这就是一个普通的枚举代码,根据电影评分给予不同的分级,但是代码冗余

使用二分法模块进行优化

import bisect
@property
def rank(self):
    # 已经排好序的评级分界点
    breakpoints = (6, 7, 8, 8.5)
    # 各评分区间级别名
    grades = ('D', 'C', 'B', 'A', 'S')
    index = bisect.bisect(breakpoints, float(self.rating))
    return grades[index]
4.2.2 使用字典优化分支
def get_sorted_movies(movies, sorting_type):
    if sorting_type == 'name':
        sorted_movies = sorted(movies, key=lambda movie: movie.name.lower())
    elif sorting_type == 'rating':
        sorted_movies = sorted(
            movies, key=lambda movie: float(movie.rating), reverse=True
        )
    elif sorting_type == 'year':
        sorted_movies = sorted(
            movies, key=lambda movie: movie.year, reverse=True
        )
    elif sorting_type == 'random':
        sorted_movies = sorted(movies, key=lambda movie: random.random())
    else:
        raise RuntimeError(f'Unknown sorting type: {sorting_type}')
    return sorted_movies

我们发现每一个分支都基本一样:

都是对sorting_type做等值判断(sorting_type == 'name')

逻辑也大同小异——都是调用sorted()函数,只是key和reverse参数略有不同

所以我们考虑用字典去优化:

sorting_algos = {
    # sorting_type: (key_func, reverse)
    'name': (lambda movie: movie.name.lower(), False),
    'rating': (lambda movie: float(movie.rating), True),
    'year': (lambda movie: movie.year, True),
    'random': (lambda movie: random.random(), False),
}

 4.3建议

4.3.1尽量避免多层嵌套

这些多层嵌套可以用一个简单的技巧来优化——“提前返回”。

“提前返回”指的是:当你在编写分支时,首先找到那些会中断执行的条件,把它们移到函数的最前面,然后在分支里直接使用return或raise结束执行。

4.3.2别写太复杂的表达式

如果表达式很长很复杂:

我们需要对条件表达式进行简化,把它们封装成函数或者对应的类方法,这样才能提升分支代码的可读性

4.3.3使用德摩根定律

简单来说,“德摩根定律”告诉了我们这么一件事:not A or not B等价于not (A and B)。

if not A or not B:
    pass
#可以改写成
if not (A and B):
    pass

这样写少了一个not变成更容易理解

4.3.4使用all() any()函数构建条件表达式

· all(iterable):仅当iterable中所有成员的布尔值都为真时返回True,否则返回False。

· any(iterable):只要iterable中任何一个成员的布尔值为真就返回True,否则返回False。

def all_numbers_gt_10(numbers):
    """仅当序列中所有数字都大于10 时,返回 True"""
    if not numbers:
        return False

    for n in numbers:
        if n <= 10:
            return False
    return True

#改写后
def all_numbers_gt_10_2(numbers):
    return bool(numbers) and all(n > 10 for n in numbers)
4.3.5 or的短路特性
#or最有趣的地方是它的“短路求值”特性。比如在下面的例子里,1 / 0永远不会被执行,也就意味着不会抛出ZeroDivisionError异常
True or (1 / 0)

所以我们利用这个特性可以简化一些分支

context = {}
# 仅当 extra_context 不为 None 时,将其追加进 context 中
if extra_context:
    context.update(extra_context)

#优化后
context.update(extra_context or {})

 5异常处理

5.1获取原谅比许可更简单

在Python世界里,EAFP指不做任何事前检查,直接执行操作,但在外层用try来捕获可能发生的异常。

def changeInt(value):
    """Try to convert the input to an integer"""
    try:
        return int(value)
    except TypeError:
        print(f'type error:{type(value)} is invalid')
    except ValueError:
        print(f'value error:{value} is invalid')
    finally:
        print('function completed')
5.1.1把最小的报错更精确的except放在最前面

如果把最大的报错放在最前面会导致所有的报错都报的同一个异常,其他都不会被触发

5.1.2使用else注意点
try:
    oneBranch()
except Exception as e:
    print("error")
else:
    print("branch succeeded")
  • 异常捕获语句里的else表示:仅当try语句块里没抛出任何异常时,才执行else分支下的内容,效果就像在try最后增加一个标记变量一样
  • 和finally语句不同,假如程序在执行try代码块时碰到了return或break等跳转语句,中断了本次异常捕获,那么即便代码没抛出任何异常,else分支内的逻辑也不会被执行。
5.1.3使用空raise语句

当一个空raise语句出现在except块里时,它会原封不动地重新抛出当前异常

try:
    oneBranch()
except Exception as e:
    print("error")
    raise 
else:
    print("branch succeeded")
5.1.4使用上下文管理器

 有一个关键字和异常处理也有着密切的关系,它就是with

with是一个神奇的关键字,它可以在代码中开辟一段由它管理的上下文,并控制程序在进入和退出这段上下文时的行为。

比如在上面的代码里,这段上下文所附加的主要行为就是:进入时打开某个文件并返回文件对象,退出时关闭该文件对象。

class DummyContext:
    def __init__(self, name):
        self.name = name

    def __enter__(self):
        #enter会在进入管理器被调用
        #返回接口
        return f'{self.name}'

    def __exit__(self, exc_type, exc_val, exc_tb):
        #退出会被调用
        print('Exiting DummyContext')
        return False
with DummyContext('HelloWorld') as name:
    print(f'Name:{name}')

#Name:HelloWorld
#Exiting DummyContext

上下文管理器功能强大、用处很多,其中最常见的用处之一,就是简化异常处理工作

正如上方5.1的例子我们用with来简化finally

def changeInt(value):
    """Try to convert the input to an integer"""
    with DummyContext():
        try:
            return int(value)
        except TypeError:
            print(f'type error:{type(value)} is invalid')
        except ValueError:
            print(f'value error:{value} is invalid')

class DummyContext:
    def __enter__(self):
        #enter会在进入管理器被调用
        #返回接口
        return True

    def __exit__(self, exc_type, exc_val, exc_tb):
        #退出会被调用
        print('function completed')
        return False

print(changeInt(3))
#function completed
#3
5.1.5使用with用于忽略异常
try:
    func()
except :
    pass

虽然这样的代码很简单,但没法复用。当项目中有很多地方要忽略这类异常时,这些try/except语句就会分布在各个角落,看上去非常凌乱。

class DummyContext:
    def __enter__(self):
        #enter会在进入管理器被调用
        #返回接口
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        #退出会被调用
        if exc_type == NameError:
            return True
        return False


with DummyContext() as c:
    func()

当你想忽略NameError异常时,只要把代码用with语句包裹起来即可

在代码执行时,假如with管辖的上下文内没有抛出任何异常,那么当解释器触发__exit__方法时,上面的三个参数值都是None;

但如果有异常抛出,这三个参数就会变成该异常的具体内容。

(1) exc_type:异常的类型。

(2) exc_value:异常对象。

(3) traceback:错误的堆栈对象。

此时,程序的行为取决于__exit__方法的返回值。如果__exit__返回了True,那么这个异常就会被当前的with语句压制住,不再继续抛出,达到“忽略异常”的效果;如果__exit__返回了False,那这个异常就会被正常抛出,交由调用方处理。

5.1.6使用contextmanager装饰器

虽然上下文管理器很好用,但定义一个符合协议的管理器对象其实挺麻烦的——得首先创建一个类,然后实现好几个魔法方法。

为了简化这部分工作,Python提供了一个非常好用的工具:@contextmanager装饰器

from contextlib import contextmanager

@contextmanager
def create_conn_obj(host, port, timeout=None):
    """创建连接对象,并在退出上下文时自动关闭"""    
    conn = create_conn()    
    try: 
        yield conn
    finally: 
        conn.close()

以yield关键字为界,yield前的逻辑会在进入管理器时执行(类似于__enter__),yield后的逻辑会在退出管理器时执行(类似于__exit__)

如果要在上下文管理器内处理异常,必须用try语句块包裹yield语句在日常工作中,我们用到的大多数上下文管理器,可以直接通过“生成器函数+@contextmanager”的方式来定义,这比创建一个符合协议的类要简单得多。

6循环和可迭代对象

在编写for循环时,不是所有对象都可以用作循环主体——只有那些可迭代(iterable)对象才行

说到可迭代对象,你最先想到的肯定是那些内置类型,比如字符串、生成器以及第3章介绍的所有容器类型,等等。

6.1iter()与next()的内置函数

当你使用for循环遍历某个可迭代对象时,其实是先调用了iter()拿到它的迭代器,然后不断地用next()从迭代器中获取值。

所以我们可以自己实现迭代器起到循环效果

intList = [1,2,3,4]

num = iter(intList)
while True:
    try:
        _int = next(num)
        print(_int)
    except StopIteration:
        break

6.2自定义迭代器

class Range7:
    #生产一个包含7或者可以被7整除
    def __init__(self,start,end):
        self.start = start
        self.end = end
        #当前位置
        self.current = start
    #__iter__:调用iter()时触发,迭代器对象总是返回自身。
    def __iter__(self):
        return self
    # __next__:调用next()时触发,通过return来返回结果
    # 没有更多内容就抛出StopIteration异常,会在迭代过程中多次触发
    def __next__(self):
        while True:
            if self.current >= self.end:
                raise StopIteration
            if self.num_is_vaild(self.current):
                ret = self.current
                self.current += 1
                return ret
            self.current += 1

    def num_is_vaild(self,num):
        #判断数字是否满足
        if not num :
            return  False
        return num % 7 ==0 or '7' in str(num)

r = Range7(0,20)
for num in r:
    print(num)

 6.3区分迭代器与可迭代对象

一个合法的迭代器,必须同时实现__iter__和__next__两个魔法方法。

可迭代对象只需要实现__iter__方法,不一定得实现__next__方法。

class Range7:
    def __init__(self,start,end):
        self.start = start
        self.end = end

    def __iter__(self):
        #返回一个新的迭代器对象
        return Range7Iterator(self)

class Range7Iterator:
    #生产一个包含7或者可以被7整除
    def __init__(self,range_obj):
        self.end = range_obj.end
        self.start = range_obj.start
        #当前位置
        self.current = range_obj.start
    #__iter__:调用iter()时触发,迭代器对象总是返回自身。
    def __iter__(self):
        return self
    # __next__:调用next()时触发,通过return来返回结果
    # 没有更多内容就抛出StopIteration异常,会在迭代过程中多次触发
    def __next__(self):
        while True:
            if self.current >= self.end:
                raise StopIteration
            if self.num_is_vaild(self.current):
                ret = self.current
                self.current += 1
                return ret
            self.current += 1

    def num_is_vaild(self,num):
        #判断数字是否满足
        if not num :
            return  False
        return num % 7 ==0 or '7' in str(num)


r = Range7(0,20)
print(tuple(r),1)
print(tuple(r),2)

 6.4生成器是迭代器

生成器还是一种简化的迭代器实现,使用它可以大大降低实现传统迭代器的编码成本。

因此在平时,我们基本不需要通过__iter__和__next__来实现迭代器,只要写上几个yield就行。

还是用上面的例子。我们用生成器来简化代码

def isRang7(num: int):

    return True if num !=0 and (num % 7 ==0 or '7' in str(num)) else False

def rang7(start: int, end: int):
    num = start
    while num < end :
        if isRang7(num):
            yield num
        num += 1

6.5使用itertools模块

看下面这个例子我们如何简化

def find_twelve(num_list1, num_list2, num_list3):
    """从3 个数字列表中,寻找是否存在和为 12 的3 个数"""    
    for num1 in num_list1:        
        for num2 in num_list2:            
            for num3 in num_list3:                
                if num1 + num2 + num3 == 12: 
                    return num1, num2, num3

我们可以使用product()函数来优化它。product()接收多个可迭代对象作为参数,然后根据它们的笛卡儿积不断生成结果

from itertools import product
print(list(product([1,2],[3,4])))#[(1, 3), (1, 4), (2, 3), (2, 4)]
from itertools import product
def find_twelve_v2(num_list1, num_list2, num_list3):    
    for num1, num2, num3 in product(num_list1, num_list2, num_list3): 
        if num1 + num2 + num3 == 12:      
            return num1, num2, num3

相比之前,新函数只用了一层for循环就完成了任务,代码变得更精练了。

7.1常用函数模块

7.1.1functools.partial

functools是一个专门用来处理函数的内置模块,其中有十几个和函数相关的有用工具

def multiply(x, y):
    return x * y
#假设我们有很多地方需要调用上面这个函数
#result = multiply(2, value)
#val = multiply(2, number)
#这些代码有一个共同的特点,这些代码有一个共同的特点
#为了简化代码
def double(value):
# 返回 multiply 函数调用结果
    return multiply(2, value)
# 调用代码变得更简单
# result = double(value)
# val = double(number)

针对这类场景,我们其实不需要像前面一样,用def去完全定义一个新函数——直接使用functools模块提供的高阶函数partial()就行。

def multiply(x, y):
    return x * y

import functools
double = functools.partial(multiply,2)
print(double(3))#6
7.1.2functools.lru_cache()

为了提高效率,给这类慢函数加上缓存是比较常见的做法。

lru即“最近最少使用”(least recently used,LRU)算法丢掉旧缓存,释放内存

下面模拟一个慢函数

import time
from functools import lru_cache
@lru_cache(maxsize=None)
def slow_func():
    time.sleep(10)
    return 1

第一次缓存没有命中,耗时比较长

第二个调用相同函数,就不会触发函数内部逻辑,结果直接返回

在使用lru_cache()装饰器时,可以传入一个可选的maxsize参数,该参数代表当前函数最多可以保存多少个缓存结果。

默认情况下,maxsize的值为128。如果你把maxsize设置为None,函数就会保存每一个执行结果,不再剔除任何旧缓存。这时如果被缓存的内容太多,就会有占用过多内存的风险。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK