源码网商城,靠谱的源码在线交易网站 我的订单 购物车 帮助

源码网商城

分享一下如何编写高效且优雅的 Python 代码

  • 时间:2020-03-28 09:27 编辑: 来源: 阅读:
  • 扫一扫,手机访问
摘要:分享一下如何编写高效且优雅的 Python 代码
本文部分提炼自书籍:《Effective Python》&《Python3 Cookbook》,但也做出了修改,并加上了作者自己的理解和运用中的最佳实践。 全文约 9956 字,读完可能需要 24 分钟。 Pythonic列表切割 [code]list[start:end:step][/code] [quote][/quote] 如果从列表开头开始切割,那么忽略 start 位的 0,例如[code]list[:4][/code] 如果一直切到列表尾部,则忽略 end 位的 0,例如[code]list[3:][/code] 切割列表时,即便 start 或者 end 索引跨界也不会有问题 列表切片不会改变原列表。索引都留空时,会生成一份原列表的拷贝 [img]http://files.jb51.net/file_images/article/201709/2017090721361415.png[/img] 列表推导式 使用列表推导式来取代[code]map[/code]和[code]filter[/code] [img]http://files.jb51.net/file_images/article/201709/2017090721361416.jpg[/img] 不要使用含有两个以上表达式的列表推导式 [img]http://files.jb51.net/file_images/article/201709/2017090721361417.jpg[/img] 数据多时,列表推导式可能会消耗大量内存,此时建议使用生成器表达式 [img]http://files.jb51.net/file_images/article/201709/2017090721361418.jpg[/img] 迭代 需要获取 index 时使用[code]enumerate[/code] [code]enumerate[/code]可以接受第二个参数,作为迭代时加在[code]index[/code]上的数值 [img]http://files.jb51.net/file_images/article/201709/2017090721361419.png[/img] 用[code]zip[/code]同时遍历两个迭代器 [img]http://files.jb51.net/file_images/article/201709/2017090721361520.png[/img] [code]zip[/code]遍历时返回一个元组 [img]http://files.jb51.net/file_images/article/201709/2017090721361521.png[/img] 关于[code]for[/code]和[code]while[/code]循环后的[code]else[/code]块 循环[b]正常结束[/b]之后会调用[code]else[/code]内的代码 循环里通过[code]break[/code]跳出循环,则不会执行[code]else[/code] 要遍历的序列为空时,立即执行[code]else[/code] [img]http://files.jb51.net/file_images/article/201709/2017090721361522.png[/img] 反向迭代 对于普通的序列(列表),我们可以通过内置的[code]reversed()[/code]函数进行反向迭代: [img]http://files.jb51.net/file_images/article/201709/2017090721361523.jpg[/img] 除此以外,还可以通过实现类里的[code]__reversed__[/code]方法,将类进行反向迭代: [img]http://files.jb51.net/file_images/article/201709/2017090721361524.jpg[/img] [code]try/except/else/finally[/code] 如果[code]try[/code]内没有发生异常,则调用[code]else[/code]内的代码 [code]else[/code]会在[code]finally[/code]之前运行 最终一定会执行[code]finally[/code],可以在其中进行清理工作 函数使用装饰器 装饰器用于在不改变原函数代码的情况下修改已存在的函数。常见场景是增加一句调试,或者为已有的函数增加[code]log[/code]监控 举个栗子: [img]http://files.jb51.net/file_images/article/201709/2017090721361525.jpg[/img] 除此以外,还可以编写接收参数的装饰器,其实就是在原本的装饰器上的外层又嵌套了一个函数: [img]http://files.jb51.net/file_images/article/201709/2017090721361526.jpg[/img] 但是像上面那样使用装饰器的话有一个问题: [img]http://files.jb51.net/file_images/article/201709/2017090721361527.png[/img] 也就是说原函数已经被装饰器里的[code]new_fun[/code]函数替代掉了。调用经过装饰的函数,相当于调用一个新函数。查看原函数的参数、注释、甚至函数名的时候,只能看到装饰器的相关信息。为了解决这个问题,我们可以使用 Python 自带的[code]functools.wraps[/code]方法。 [code]functools.wraps[/code]是个很 hack 的方法,它本事作为一个装饰器,做用在装饰器内部将要返回的函数上。也就是说,它是装饰器的装饰器,并且以原函数为参数,作用是保留原函数的各种信息,使得我们之后查看被装饰了的原函数的信息时,可以保持跟原函数一模一样。 [img]http://files.jb51.net/file_images/article/201709/2017090721361528.jpg[/img] 此外,有时候我们的装饰器里可能会干不止一个事情,此时应该把事件作为额外的函数分离出去。但是又因为它可能仅仅和该装饰器有关,所以此时可以构造一个装饰器类。原理很简单,主要就是编写类里的[code]__call__[/code]方法,使类能够像函数一样的调用。 [img]http://files.jb51.net/file_images/article/201709/2017090721361529.jpg[/img] 使用生成器 [quote] 考虑使用生成器来改写直接返回列表的函数 [/quote] [img]http://files.jb51.net/file_images/article/201709/2017090721361630.jpg[/img] 用这种方法有几个小问题: 每次获取到符合条件的结果,都要调用[code]append[/code]方法。但实际上我们的关注点根本不在这个方法,它只是我们达成目的的手段,实际上只需要[code]index[/code]就好了 返回的[code]result[/code]可以继续优化 数据都存在[code]result[/code]里面,如果数据量很大的话,会比较占用内存 因此,使用生成器[code]generator[/code]会更好。生成器是使用[code]yield[/code]表达式的函数,调用生成器时,它不会真的执行,而是返回一个迭代器,每次在迭代器上调用内置的[code]next[/code]函数时,迭代器会把生成器推进到下一个[code]yield[/code]表达式: [img]http://files.jb51.net/file_images/article/201709/2017090721361631.png[/img] 获取到一个生成器以后,可以正常的遍历它: [img]http://files.jb51.net/file_images/article/201709/2017090721361632.jpg[/img] 如果你还是需要一个列表,那么可以将函数的调用结果作为参数,再调用[code]list[/code]方法 [img]http://files.jb51.net/file_images/article/201709/2017090721361633.png[/img] 可迭代对象 需要注意的是,普通的迭代器只能迭代一轮,一轮之后重复调用是无效的。解决这种问题的方法是,你可以[b]定义一个可迭代的容器类[/b]: [img]http://files.jb51.net/file_images/article/201709/2017090721361634.png[/img] 这样的话,将类的实例迭代重复多少次都没问题: [img]http://files.jb51.net/file_images/article/201709/2017090721361635.jpg[/img] 但要注意的是,仅仅是实现[code]__iter__[/code]方法的迭代器,只能通过[code]for[/code]循环来迭代;想要通过[code]next[/code]方法迭代的话则需要使用[code]iter[/code]方法: [img]http://files.jb51.net/file_images/article/201709/2017090721361636.png[/img] 使用位置参数 有时候,方法接收的参数数目可能不一定,比如定义一个求和的方法,至少要接收两个参数: [img]http://files.jb51.net/file_images/article/201709/2017090721361737.jpg[/img] 对于这种接收参数数目不一定,而且不在乎参数传入顺序的函数,则应该利用位置参数[code]*args[/code]: [img]http://files.jb51.net/file_images/article/201709/2017090721361738.jpg[/img] 但要注意的是,不定长度的参数[code]args[/code]在传递给函数时,需要先转换成元组[code]tuple[/code]。这意味着,如果你将一个生成器作为参数带入到函数中,生成器将会先遍历一遍,转换为元组。这可能会消耗大量内存: [img]http://files.jb51.net/file_images/article/201709/2017090721361739.png[/img] 使用关键字参数 关键字参数可提高代码可读性 可以通过关键字参数给函数提供默认值 便于扩充函数参数 [b]定义只能使用关键字参数的函数[/b] 普通的方式,在调用时不会强制要求使用关键字参数 [img]http://files.jb51.net/file_images/article/201709/2017090721361740.jpg[/img] 使用 [i]Python3[/i] 中强制关键字参数的方式 [img]http://files.jb51.net/file_images/article/201709/2017090721361741.jpg[/img] 使用 [i]Python2[/i] 中强制关键字参数的方式 [img]http://files.jb51.net/file_images/article/201709/2017090721361742.jpg[/img] 关于参数的默认值 算是老生常谈了:[b]函数的默认值只会在程序加载模块并读取到该函数的定义时设置一次[/b] 也就是说,如果给某参数赋予动态的值( 比如[code][][/code]或者[code]{}[/code]),则如果之后在调用函数的时候给参数赋予了其他参数,则以后再调用这个函数的时候,之前定义的默认值将会改变,成为上一次调用时赋予的值: [img]http://files.jb51.net/file_images/article/201709/2017090721361743.png[/img] 因此,更推荐使用[code]None[/code]作为默认参数,在函数内进行判断之后赋值: [img]http://files.jb51.net/file_images/article/201709/2017090721361744.jpg[/img] 类[code]__slots__[/code] 默认情况下,Python 用一个字典来保存一个对象的实例属性。这使得我们可以在运行的时候动态的给类的实例添加新的属性: [img]http://files.jb51.net/file_images/article/201709/2017090721361745.png[/img] 然而这个字典浪费了多余的空间 -— 很多时候我们不会创建那么多的属性。因此通过[code]__slots__[/code]可以告诉 Python 不要使用字典而是固定集合来分配空间。 [img]http://files.jb51.net/file_images/article/201709/2017090721361746.jpg[/img] [code]__call__[/code] 通过定义类中的[code]__call__[/code]方法,可以使该类的实例能够像普通函数一样调用。 [img]http://files.jb51.net/file_images/article/201709/2017090721361747.jpg[/img] 通过这种方式实现的好处是,可以通过类的属性来保存状态,而不必创建一个闭包或者全局变量。 [code]@classmethod[/code] & [code]@staticmethod[/code] [code]@classmethod[/code]和[code]@staticmethod[/code]很像,但他们的使用场景并不一样。 类内部普通的方法,都是以[code]self[/code]作为第一个参数,代表着通过实例调用时,将实例的作用域传入方法内; [code]@classmethod[/code]以[code]cls[/code]作为第一个参数,代表将类本身的作用域传入。无论通过类来调用,还是通过类的实例调用,默认传入的第一个参数都将是类本身 [code]@staticmethod[/code]不需要传入默认参数,类似于一个普通的函数 来通过实例了解它们的使用场景: 假设我们需要创建一个名为[code]Date[/code]的类,用于储存 年/月/日 三个数据 [img]http://files.jb51.net/file_images/article/201709/2017090721361848.jpg[/img] 上述代码创建了[code]Date[/code]类,该类会在初始化时设置[code]day/month/year[/code]属性,并且通过[code]property[/code]设置了一个[code]getter[/code],可以在实例化之后,通过[code]time[/code]获取存储的时间: [img]http://files.jb51.net/file_images/article/201709/2017090721361849.png[/img] 但如果我们想改变属性传入的方式呢?毕竟,在初始化时就要传入年/月/日三个属性还是很烦人的。能否找到一个方法,在不改变现有接口和方法的情况下,可以通过传入[code]2016-11-09[/code]这样的字符串来创建一个[code]Date[/code]实例? 你可能会想到这样的方法: [img]http://files.jb51.net/file_images/article/201709/2017090721361850.png[/img] 但不够好: 在类外额外多写了一个方法,每次还得格式化以后获取参数 这个方法也只跟[code]Date[/code]类有关 没有解决传入参数过多的问题 此时就可以利用[code]@classmethod[/code],在类的内部新建一个格式化字符串,并返回类的实例的方法: [img]http://files.jb51.net/file_images/article/201709/2017090721361851.jpg[/img] 这样,我们就可以通过[code]Date[/code]类来调用[code]from_string[/code]方法创建实例,并且不侵略、修改旧的实例化方式: [img]http://files.jb51.net/file_images/article/201709/2017090721361852.png[/img] 好处: 在[code]@classmethod[/code]内,可以通过[code]cls[/code]参数,获取到跟外部调用类时一样的便利 可以在其中进一步封装该方法,提高复用性 更加符合面向对象的编程方式 而[code]@staticmethod[/code],因为其本身类似于普通的函数,所以可以把和这个类相关的 helper 方法作为[code]@staticmethod[/code],放在类里,然后直接通过类来调用这个方法。 [img]http://files.jb51.net/file_images/article/201709/2017090721361853.png[/img] 将与日期相关的辅助类函数作为[code]@staticmethod[/code]方法放在[code]Date[/code]类内后,可以通过类来调用这些方法: [img]http://files.jb51.net/file_images/article/201709/2017090721361854.png[/img] 创建上下文管理器 上下文管理器,通俗的介绍就是:在代码块执行前,先进行准备工作;在代码块执行完成后,做收尾的处理工作。[code]with[/code]语句常伴随上下文管理器一起出现,经典场景有: [img]http://files.jb51.net/file_images/article/201709/2017090721361855.png[/img] 通过[code]with[/code]语句,代码完成了文件打开操作,并在调用结束,或者读取发生异常时自动关闭文件,即完成了文件读写之后的处理工作。如果不通过上下文管理器的话,则会是这样的代码: [img]http://files.jb51.net/file_images/article/201709/2017090721361856.png[/img] 比较繁琐吧?所以说使用上下文管理器的好处就是,通过调用我们预先设置好的回调,自动帮我们处理代码块开始执行和执行完毕时的工作。而通过自定义类的[code]__enter__[/code]和[code]__exit__[/code]方法,我们可以自定义一个上下文管理器。 [img]http://files.jb51.net/file_images/article/201709/2017090721361957.jpg[/img] 然后可以以这样的方式进行调用: [img]http://files.jb51.net/file_images/article/201709/2017090721361958.png[/img] 在调用的时候: [code]with[/code]语句先暂存了[code]ReadFile[/code]类的[code]__exit__[/code]方法 然后调用[code]ReadFile[/code]类的[code]__enter__[/code]方法 [code]__enter__[/code]方法打开文件,并将结果返回给[code]with[/code]语句 上一步的结果被传递给[code]file_read[/code]参数 在[code]with[/code]语句内对[code]file_read[/code]参数进行操作,读取每一行 读取完成之后,[code]with[/code]语句调用之前暂存的[code]__exit__[/code]方法 [code]__exit__[/code]方法关闭了文件 要注意的是,在[code]__exit__[/code]方法内,我们关闭了文件,但最后返回[code]True[/code],所以错误不会被[code]with[/code]语句抛出。否则[code]with[/code]语句会抛出一个对应的错误。
  • 全部评论(0)
联系客服
客服电话:
400-000-3129
微信版

扫一扫进微信版
返回顶部