• 如何正确遵守 Python 代码规范


    前言

    无规矩不成方圆,代码亦是如此,本篇文章将会介绍一些自己做项目时遵守的较为常用的 Python 代码规范。

    命名

    大小写

    • 模块名写法: module_name

    • 包名写法: package_name

    • 类名: ClassName

    • 方法名: method_name

    • 异常名: ExceptionName

    • 函数名: function_name

    • 全局常量名: GLOBAL_CONSTANT_NAME

    • 全局变量名: global_var_name

    • 实例名: instance_var_name

    • 函数参数名: function_parameter_name

    • 局部变量名: local_var_name

    命名约定

    1. 函数名,变量名和文件名应该是描述性的,尽量避免缩写,除了计数器和迭代器、作为 try/except 中异常声明的 e 以及作为 with 语句中文件句柄的 f.
    2. 用单下划线(_)开头表示变量或函数是 protected 的,不应该被外部访问(除了子类).

    注释

    函数和方法

    一个函数必须要有文档字符串, 除非它满足以下条件:

    1. 外部不可见
    2. 非常短小
    3. 简单明了

    文档字符串应该包含函数做什么,以及输入和输出的详细描述。通常,不应该描述“怎么做”,除非是一些复杂的算法。文档字符串应该提供足够的信息,当别人编写代码调用该函数时,他不需要看一行代码,只要看文档字符串就可以了。

    文档字符串有多种风格,比如 Google 风格和 Numpy 风格,这里比较推荐 Numpy 风格的文档字符串,基本写法如下述代码所示:

    复制def download_song(song_info: SongInfo, save_dir: str, quality=SongQuality.STANDARD) -> str:
        """ download online music to local
    
        Parameters
        ----------
        song_info: SongInfo
            song information
    
        save_dir: str
            directory to save the downloaded audio file
    
        quality: SongQuality
            song sound quality
    
        Returns
        -------
        song_path: str
            save path of audio file, empty string when the download fails
    
        Raises
        ------
        AudioQualityError:
            thrown when the sound quality is illegal
        """
        pass
    

    块注释和行注释

    最需要写注释的是代码中那些技巧性的部分,对于复杂的操作,应该在其操作开始前写上若干行注释。对于不是一目了然的代码, 应在其行尾添加注释。为了提高可读性, 注释应该至少离开代码 2 个空格。

    复制# We use a weighted dictionary search to find out where i is in
    # the array.  We extrapolate position based on the largest num
    # in the array and the array size and then do binary search to
    # get the exact number.
    if i & (i-1) == 0:        # True if i is 0 or a power of 2.
    

    另一方面,绝不要描述代码,阅读代码的人可能比你更懂 Python,他只是不知道你的代码要做什么.

    复制# BAD COMMENT: increase i
    i += 1
    

    缩进

    用 4 个空格来缩进代码,绝对不要用 tab, 也不要 tab 和空格混用。对于行连接的情况,应该垂直对齐换行的元素, 或者使用 4 空格的悬挂式缩进(这时第一行不应该有参数)。

    哎呦不错哦:

    复制# 垂直对齐参数
    foo = long_function_name(var_one, var_two,
                             var_three, var_four)
    
    # 字典内垂直对齐
    foo = {
        "long_dictionary_key": value1 +
                               value2,
        ...
    }
    
    # 4 个空格的悬挂缩进,左括号后面没有参数
    foo = long_function_name(
        var_one,
        var_two,
        var_three,
        var_four
    )
    
    # 字典内 4 个空格的悬挂缩进
    foo = {
        "long_dictionary_key":
            long_dictionary_value,
        ...
    }
    

    那种事情不要啊:

    复制# 左括号后不能携带参数
    foo = long_function_name(var_one, var_two,
        var_three, var_four
    )
    
    
    # 禁止 2 个空格的悬挂式缩进
    foo = long_function_name(
      var_one,
      var_two,
      var_three,
      var_four
    )
    
    # 字典内需要使用悬挂缩进
    foo = {
        "long_dictionary_key":
        long_dictionary_value,
        ...
    }
    

    编写代码时不能缩进过多的层级,一般不要超过 4 层,不然会造成阅读困难。Python 中的缩进一般来自于 if-elseforwhile 等语句块,一种减少缩进的方法是写 if 就不写 else,比如:

    复制def update_by_ids(self, entities: List[Entity]) -> bool:
        """ update multi records
    
        Parameters
        ----------
        entities: List[Entity]
            entity instances
    
        Returns
        -------
        success: bool
            whether the update is successful
        """
        # 不满足条件直接返回默认值
        if not entities:
            return True
    
        # 假设这是一堆很复杂的业务代码
        db = self.get_database()
        db.transaction()
    
        # 创建 sql 语句
        id_ = self.fields[0]
        values = ','.join([f'{i} = :{i}' for i in self.fields[1:]])
        sql = f"UPDATE {self.table} SET {values} WHERE {id_} = :{id_}"
        self.query.prepare(sql)
    
        return db.commit()
    

    还可以使用 continue 来减少 forwhile 中不必要的缩进:

    复制bboxes = []
    for contour in contours:
        if cv.contourArea(contour) < 500:
            continue
    
        x, y, bw, bh = cv.boundingRect(contour)
        bboxes.append([x, y, bw, bh])
    

    换行

    顶级定义之间空两行,比如函数或者类定义。 方法定义,类定义与第一个方法之间,都应该空一行。函数或方法中,某些地方要是你觉得合适,就空一行,比如在 if-elseforwhile 的后面空一行,以及在不同的逻辑之间加空行:

    复制def download_cover(url: str, singer: str, album: str) -> str:
        """ download online album cover
    
        Parameters
        ----------
        url: str
            the url of online album cover
    
        singer: str
            singer name
    
        album: str
            album name
    
        Returns
        -------
        save_path: str
            save path of album cover, empty string when the download fails
        """
        if not url.startswith('http'):
            return ''
    
        # request data
        headers = {
            'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) '
                            'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36'
        }
        response = requests.get(url, headers=headers)
        response.raise_for_status()
        pic_data = response.content
    
        # save album cover
        return Cover(singer, album).save(pic_data)
    

    导包

    导入总应该放在文件顶部, 位于模块注释和文档字符串之后, 模块全局变量和常量之前。导入应该按照 标准库 —> 第三方包 —> 项目代码 的顺序导入:

    复制# coding:utf-8
    import base64
    import json
    from pathlib import Path
    from typing import Union
    
    import requests
    from common.database.entity import SongInfo
    from common.url import FakeUrl
    
    from .crawler_base import CrawlerBase, MvQuality
    

    函数长度

    不对函数长度做硬性限制。但是如果一个函数超过了 40 行,应该考虑一下能否在不损害程序结构的情况下对其进行分解。因为即使长函数运行良好,几个月后可能会有人修改它并添加一些新的行为,这容易产生难以发现的 bug。保持函数的简练,使其更加容易阅读和修改。当遇到一些很长的函数时,若发现调试比较困难或是想在其他地方使用函数的一部分功能,不妨考虑将这个长函数进行拆分。

    类型注释

    1. 公共的 API 需要注释

    2. 对于容易出现类型相关的错误的代码进行注释,比如下述代码的 image 可能为 numpy 数组,也可能是 Image 类型,究竟是哪种类型需要指明:

      复制def draw_bboxes(image: np.ndarray, bboxes: Union[np.ndarray, list], labels: List[str]) -> Image:
          pass
      
    3. 可以使用行尾注释 # type:

      复制persons = []  # type: List[Person]
      

    参数和返回值类型

    当函数需要传入或者返回多个值,可以考虑将这些相关的值作为数据成员封装到一个类中。如下述代码所示,使用 python3.7 提供的 dataclass 装饰器可以十分方便地创建一个实体类,接着可以传入传出实体类的实例:

     str:
            if not FakeUrl.isFake(song_info.file):
                return song_info.file
    
            # send request for play url
            rid = KuWoFakeSongUrl.getId(song_info.file)
            url = f'http://www.kuwo.cn/api/v1/www/music/playUrl?mid={rid}&type=convert_url3&br=128kmp3'
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            play_url = json.loads(response.text)['data']['url']
    
            return play_url
    ">复制from dataclasses import dataclass
    
    @dataclass
    class SongInfo:
        """ Song information """
        file: str = None
        title: str = None
        singer: str = None
        album: str = None
        year: int = None
        genre: str = None
        duration: int = None
        track: int = None
        track_total: int = None
        disc: int = None
        disc_total: int = None
        create_time: int = None
        modified_time: int = None
    
    
    class SongInfoReader(SongInfoReaderBase):
        """ Song information reader """
    
        def read(self, file: Union[str, Path]):
            if not isinstance(file, Path):
                file = Path(file)
    
            tag = TinyTag.get(file)
    
            file_ = str(file).replace('\\', '/')
            title = tag.title or file.stem
            singer = tag.artist or self.singer
            album = tag.album or self.album
            year = self.__get_year(tag, file)
            genre = tag.genre or self.genre
            duration = int(tag.duration)
            track = self._parseTrack(tag.track or self.track)
            track_total = int(tag.track_total or self.track_total)
            disc = int(tag.disc or self.disc)
            disc_total = int(tag.disc_total or self.disc_total)
            create_time = int(file.stat().st_ctime)
            modified_time = int(file.stat().st_mtime)
    
            return SongInfo(
                file=file_,
                title=title,
                singer=singer,
                album=album,
                year=year,
                genre=genre,
                duration=duration,
                track=track,
                track_total=track_total,
                disc=disc,
                disc_total=disc_total,
                createTime=create_time,
                modifiedTime=modified_time
            )
    
    
    class KuWoMusicCrawler:
        """ Kuwo music crawler """
    
        def get_song_url(self, song_info: SongInfo) -> str:
            if not FakeUrl.isFake(song_info.file):
                return song_info.file
    
            # send request for play url
            rid = KuWoFakeSongUrl.getId(song_info.file)
            url = f'http://www.kuwo.cn/api/v1/www/music/playUrl?mid={rid}&type=convert_url3&br=128kmp3'
            response = requests.get(url, headers=self.headers)
            response.raise_for_status()
            play_url = json.loads(response.text)['data']['url']
    
            return play_url
    

    千万不要图一时方便而把这些字段放在字典或者列表之中,然后作为函数的参数或者返回值。字典一时爽,重构火葬场。

    写在最后

    如果能遵守上述规范,相信代码不会那么容易散发出难闻的气味。关于更多代码规范,可以参见 Google 开源项目风格指南,本文也参考了指南的部分内容。如果想写出更加优雅的代码,可以阅读设计模式相关的书籍和博客,这里推荐《精通Python设计模式》和教程 Refactoring GURU,同时强烈推荐下列油管视频教程:

    以上~~

  • 相关阅读:
    Redis入门完整教程:RDB持久化
    一文让你彻底了解多线程
    自动驾驶专题介绍 —— 车辆尺寸
    艾体宝案例 | 使用Redis和Spring Ai构建rag应用程序
    C++运算符重载+,*在QT中的实现演示
    柔性数组笔记
    ​力扣解法汇总481. 神奇字符串
    【0109】PostgreSQL配置WAL Archive
    【垃圾回收概述及算法】
    第十二届钧瓷文化旅游节主题曲:让世界看见钧瓷的魅力
  • 原文地址:https://www.cnblogs.com/zhiyiYo/p/16894508.html