• 让Python更优雅更易理解


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

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

    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具名元祖
    元组经常用来存放结构化数据,但只能通过数字来访问元组成员其实特别不方便

    初始化具名函数
    可以通过数字索引来访问
    也可以通过名称来访问
    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 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)
    
    • 1
    • 2

    复制代码
    或者可以在类中实现__bool__

    复制代码
    from typing import List
    class Length:
    def init(self,items: List[str]):
    self.items = items

    def __bool__(self):
        return len(self.items)>2
    
    • 1
    • 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'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    使用二分法模块进行优化

    复制代码
    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
    
    • 1
    • 2
    • 3
    • 4

    #改写后
    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    复制代码
    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
    
    • 1
    • 2
    • 3
    • 4

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    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)
    
    • 1
    • 2
    • 3

    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)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    r = Range7(0,20)
    print(tuple®,1)
    print(tuple®,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
    
    • 1

    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.函数
    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)
    
    • 1

    调用代码变得更简单

    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,函数就会保存每一个执行结果,不再剔除任何旧缓存。这时如果被缓存的内容太多,就会有占用过多内存的风险。

  • 相关阅读:
    【Leetcode】1030. Matrix Cells in Distance Order
    Perforce发布《2023游戏开发与设计现状报告》,为游戏开发行业提供参考
    electron笔记无边框窗口、DLL调用、DLL函数返回指针
    vue获取外网IP、java后端及nginx多次转发获取真实IP
    JUL 日志
    蜘蛛的依旧疯狂与园子的新畅想:尝试放出被屏蔽的百度蜘蛛网段
    Linux CP文件夹略过目录的解决
    c++ SQLite 特别好用的库使用实例-查询(2)
    c语言练习48:总结字符函数和字符串函数
    无锡布里渊——厘米级分布式光纤-锅炉安全监测解决方案
  • 原文地址:https://blog.csdn.net/weixin_43214644/article/details/126322433