python实践中杂项

python实践中杂项

python的模块化

sys.path可以查看项目的寻找模块的路径

pycharm默认会将当前项目的根路径加入到sys.path中,并且是加入到很前的位置,即程序跑起来位置的,下一个位置。

在这里插入图片描述

Python 是脚本语言,和 C++、Java 最大的不同在于,不需要显式提供 main() 函数入口。那么下面的代码作用是什么呢?

 if __name__ == '__main__':
    print('Hello World')

import 在导入文件的时候,会自动把所有暴露在外面的代码全都执行一遍,为了importst时不让外面的代码执行一遍,可以放到main里面去。为什么呢?其实,__name__ 作为 Python 的魔术内置参数,本质上是模块对象的一个属性。我们使用 import 语句时,__name__ 就会被赋值为该模块的名字,自然就不等于 __main__了。只有在跑起来的那个脚本__name__才是main

导包原则:在大型工程中模块化非常重要,模块的索引要通过绝对路径来做,而绝对路径从程序的根目录开始。即设定根路径到sys.path中,设置方式可以查一下。pycharm自动做到了这点。

python中的类

__开头的属性是私有属性

self.__context = context # 私有属性

类函数、成员函数和静态函数。静态函数与类没有什么关联,最明显的特征便是,静态函数的第一个参数没有任何特殊性,静态函数可以用来做一些简单独立的任务,既方便测试,也能优化代码结构。静态函数还可以通过在函数前一行加上 @staticmethod 来表示,代码中也有相应的示例。类函数的第一个参数一般为 cls,表示必须传一个类进来。类函数最常用的功能是实现不同的 init 构造函数,比如上文代码中,我们使用 create_empty_book 类函数,来创造新的书籍对象,其 context 一定为 'nothing'。这样的代码,就比你直接构造要清晰一些。类似的,类函数需要装饰器 @classmethod 来声明。

成员函数则是我们最正常的类的函数,它不需要任何装饰器声明,第一个参数 self 代表当前对象的引用,可以通过此函数,来实现想要的查询 / 修改类的属性等功能。

class Document():
    
    WELCOME_STR = 'Welcome! The context for this book is {}.'
    
    def __init__(self, title, author, context):
        print('init function called')
        self.title = title
        self.author = author
        self.__context = context
    
    # 类函数
    @classmethod
    def create_empty_book(cls, title, author):
        return cls(title=title, author=author, context='nothing')
    
    # 成员函数
    def get_context_length(self):
        return len(self.__context)
    
    # 静态函数
    @staticmethod
    def get_welcome(context):
        return Document.WELCOME_STR.format(context)

类的继承

class Sub(Parent):

    def __init__(self, sub_name):
        self.sub_name = sub_name
        # 调用父类的构造函数了
        Parent.__init__(self, 'Parent')

    def print_sub_parent(self):
        print('-'.join((self.sub_name, self.name)))
       
class Parent(object):
    def __init__(self, name):
        self.name = name

python对象的比较

==是值比较,执行a == b相当于是去执行a.__eq__(b),而 Python 大部分的数据类型都会去重载__eq__这个函数,其内部的处理通常会复杂一些。比如,对于列表,__eq__函数会去遍历列表中的元素,比较它们的顺序和值是否相等。is是比较的是对象的身份标识是否相等,在python中,对象标识符能通过id(object)获取。

出于对性能优化的考虑,Python 内部会对 -5 到 256 的整型维持一个数组,起到一个缓存的作用。这样,每次你试图创建一个 -5 到 256 范围内的整型数字时,Python 都会从这个数组中返回相对应的引用,而不是重新开辟一块新的内存空间。但是,如果整型数字超过了这个范围,比如上述例子中的 257,Python 则会为两个 257 开辟两块内存区域,因此 a 和 b 的 ID 不一样,a is b就会返回 False 了。

对于不可变(immutable)的变量,如果我们之前用'=='或者'is'比较过,结果是不是就一直不变了呢?不是的,因为不可变对象可以嵌套可变对象,比如元组嵌套列表。

python的浅拷贝和深拷贝

import copy
l1 = [[1, 2], (30, 40)]
l2 = copy.deepcopy(l1)
l1.append(100)
l1[0].append(3)
 
l1
[[1, 2, 3], (30, 40), 100]
 
l2 
[[1, 2], (30, 40)]

python中的值传递和引用传递

准确地说,Python 的参数传递是赋值传递 (pass by assignment),或者叫作对象的引用传递(pass by object reference)。Python 里所有的数据类型都是对象,所以参数传递时,只是让新变量与原变量指向相同的对象而已,并不存在值传递或是引用传递一说。个人认为,这是殊途同归。不可变对象对应值传递。可以和java传递值,传递String引用做对比。

python中的装饰器

在python中,函数也是对象,我们可以把函数赋予变量,这样就可以用变量调用函数。

我们可以把函数当作参数,传入另一个函数中。

我们可以在函数里定义函数。

函数的返回值也可以是函数(闭包)

一个简单的装饰器例子

def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper
 
def greet():
    print('hello world')
 
greet = my_decorator(greet)
greet()
 
# 输出
wrapper of decorator
hello world

装饰器更优雅的表达方式
def my_decorator(func):
    def wrapper():
        print('wrapper of decorator')
        func()
    return wrapper
 
@my_decorator
def greet():
    print('hello world')
 
greet()

如果装饰器需要带有参数,通常情况下,我们会把*args和**kwargs,作为装饰器内部函数wrapper()的参数。*args和*kwargs,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式

def my_decorator(func):
    def wrapper(*args, **kwargs):
        print('wrapper of decorator')
        func(*args, **kwargs)
    return wrapper

带有自定义参数的装饰器

def repeat(num):
    def my_decorator(func):
        def wrapper(*args, **kwargs):
            for i in range(num):
                print('wrapper of decorator')
                func(*args, **kwargs)
        return wrapper
    return my_decorator
 
 
@repeat(4)
def greet(message):
    print(message)
    
greet('hello world')
 
# 输出:
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world
wrapper of decorator
hello world

装饰器后原函数还是原函数吗

不是的,他的__name会变成wrapper,为了解决这个问题,可以使用内置的装饰器@functools.wrap,它会帮助保留原函数的元信息

import functools
 
def my_decorator(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('wrapper of decorator')
        func(*args, **kwargs)
    return wrapper
    
@my_decorator
def greet(message):
    print(message)

类装饰器

类装饰器主要依赖于函数__call_(),每当你调用一个类的示例时,函数__call__()就会被执行。

__call__的用法:

stu = Stu()

# 调用了Stu类的__call__方法

stu()


​```python
class Count:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0
 
    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print('num of calls is: {}'.format(self.num_calls))
        return self.func(*args, **kwargs)
 
@Count
def example():
    print("hello world")

装饰器的嵌套

@decorator1
@decorator2
@decorator3
def func():
    ...
# 这个情况是允许的
# 等价于
decorator1(decorator2(decorator3(func)))

元类

所有的Python的用户定义类,都是type这个类的实例,用户自定义类,只不过是type类的__call__运算符重载,metaclass是type的子类,通过替换type的__call__运算符重载机制,"超越变形"正常的类。

myclass =MyClass()
# 这边这行代码其实调用的是type('MyClass', (), {'data': 1})
# 而之前我们说过对象引号直接加括号是调用__call__
# 所以他就是调用的type的__call__
# type的__call__做的事情包括以下
type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)
# 使用元类之后发生了一些变化
class = type(classname, superclasses, attributedict) 
# 变为了
class = MyMeta(classname, superclasses, attributedict)
# 而MyMeta的init进行了超越变形,他会给这个类添加一些功能,修改了行为
# 下面就是利用元类,在每次实例化类的时候都会调用add_constructor
class YAMLObjectMetaclass(type):
  def __init__(cls, name, bases, kwds):
    super(YAMLObjectMetaclass, cls).__init__(name, bases, kwds)
    if 'yaml_tag' in kwds and kwds['yaml_tag'] is not None:
      cls.yaml_loader.add_constructor(cls.yaml_tag, cls.from_yaml)
  # 省略其余定义

python的迭代器和生成器

判断一个对象是否可迭代

def is_iterable(param):
    try: 
        iter(param) 
        return True
    except TypeError:
        return False

生成器即只有在被使用的时候才会去生成对象,所以他不会像迭代器那样占用大量内存。

# ()是生成器, []是直接生成数组
list_2 = (i for i in range(100000000))

使用迭代器返回与指定元素相等的下标

def index_generator(L, target):
    for i, num in enumerate(L):
        if num == target:
            yield i

print(list(index_generator([1, 6, 2, 4, 5, 2, 8, 6, 3, 2], 2)))

这个是个生成器,你可以理解为,函数运行到yield这一行的时候,程序会从这里暂停,然后跳出,不过跳到哪里呢?答案是 next() 函数。那么 i ** k 是干什么的呢?它其实成了 next() 函数的返回值。当下次再运行到这里时,暂停的程序就会又复活了,从yield这里向下继续执行,同时注意变量i并没有被清除掉,而是会继续累加。一个Generator对象,需要使用list转换为列表后,才能用print输出。

生成器的技巧:

b = (i for i in range(5))
 
print(2 in b)
print(4 in b)
print(3 in b)
 
########## 输出 ##########
 
True
True
False

上面的(2 in b)等价于

while True:
    val = next(b)
    if val == 2:
        yield True

所以会过了4之后,想再回3是不行的。

python的协程

python的全局解释器锁

python的解释器并不是线程安全的,所以引入了全局解释器锁,也就是同一个时刻,只允许一个线程执行。当然在执行I/O操作时,如果一个线程被block了,全局解释器锁就会被释放,从而让另一个线程能够继续执行。

Asyncio工作原理

Asyncio和其他Python程序一样,单线程的,它只有一个主线程,但是可以进行多个不同任务,这里的任务,就是特殊的future对象,被一个叫做eventloop的对象所控制,这个任务只有两个状态: 一是预备状态,二是等待状态。eventloop会维护两个任务列表,分别对应这两种状态,并选取预备状态的一个任务,使其运行,一直到这个任务把控制权交还给eventloop为止。当任务把控制权交还给 event loop 时,event loop 会根据其是否完成,把任务放到预备或等待状态的列表,然后遍历等待状态列表的任务,查看他们是否完成。

Asyncio的用法

import asyncio
import aiohttp
import time

async def download_one(url):
    async with aiohttp.ClientSession() as session:
        async with session.get(url) as resp:
            print('Read {} from {}'.format(resp.content_length, url))

async def download_all(sites):
    tasks = [asyncio.create_task(download_one(site)) for site in sites]
    await asyncio.gather(*tasks)

def main():
    sites = [
        'https://en.wikipedia.org/wiki/Portal:Arts',
        'https://en.wikipedia.org/wiki/Portal:History',
        'https://en.wikipedia.org/wiki/Portal:Society',
        'https://en.wikipedia.org/wiki/Portal:Biography',
        'https://en.wikipedia.org/wiki/Portal:Mathematics',
        'https://en.wikipedia.org/wiki/Portal:Technology',
        'https://en.wikipedia.org/wiki/Portal:Geography',
        'https://en.wikipedia.org/wiki/Portal:Science',
        'https://en.wikipedia.org/wiki/Computer_science',
        'https://en.wikipedia.org/wiki/Python_(programming_language)',
        'https://en.wikipedia.org/wiki/Java_(programming_language)',
        'https://en.wikipedia.org/wiki/PHP',
        'https://en.wikipedia.org/wiki/Node.js',
        'https://en.wikipedia.org/wiki/The_C_Programming_Language',
        'https://en.wikipedia.org/wiki/Go_(programming_language)'
    ]
    start_time = time.perf_counter()
    asyncio.run(download_all(sites))
    end_time = time.perf_counter()
    print('Download {} sites in {} seconds'.format(len(sites), end_time - start_time))
    
if __name__ == '__main__':
    main()

Asyncio

要想用好Asyncio,很多情况下必须得有相应的Python库支持,比如请求http时,必须要用支持协程的http库,比如aiohttp库,它兼容Asyncio。

多线程还是Asyncio

if io_bound:
    # io密集型
    if io_slow:
        # 每个io操作很慢
        print('Use Asyncio')
    else:
        # io操作很快
        print('Use multi-threading')
else if cpu_bound:
    # cpu密集的
    print('Use multi-processing')

python的GIL

CPython解释器使用引用计数作内存管理,所有Python脚本中创建的实例,都会有一个引用技术,来记录有多少个指针,当引用计数只有0时,则会自动释放。如果有两个Python线程同时引用了a,就会造成引用计数的race condition,引用计数可能最终只增加1,这样就会造成内存被污染。

所以说,CPython 引进 GIL 其实主要就是这么两个原因:

  • 一是设计者为了规避类似于内存管理这样的复杂的竞争风险问题(race condition);
  • 二是因为 CPython 大量使用 C 语言库,但大部分 C 语言库都不是原生线程安全的(线程安全会降低性能和增加复杂度)。

为什么Python线程会主动释放GIL呢?check_interval,CPython解释器会去轮询检查线程GIL的锁住情况,每隔一段时间,Python解释器就会强制当前线程去释放GIL,这样别的线程才有机会执行。

python的垃圾内存回收机制

查看python进程的内存

def show_memory_info(hint):
    pid = os.getpid()
    p = psutil.Process(pid)
    
    info = p.memory_full_info()
    memory = info.uss / 1024. / 1024
    print('{} memory used: {} MB'.format(hint, memory))

查看python对象的内部引用计数

import sys
 
a = []
 
# 两次引用,一次来自 a,一次来自 getrefcount
print(sys.getrefcount(a))
 
def func(a):
    # 四次引用,a,python 的函数调用栈,函数参数,和 getrefcount
    print(sys.getrefcount(a))
 
func(a)
 
# 两次引用,一次来自 a,一次来自 getrefcount,函数 func 调用已经不存在
print(sys.getrefcount(a))
 
########## 输出 ##########
 
2
4
2

Python使用标记清除算法和分代收集,来启用针对循环引用的自动垃圾回收。标记清除可以类似于Java的GC root,分代收集也是类似于Java的分代。python的垃圾收集是以引用计数+不可达+分代实现的。

调试内存泄漏

推荐使用objgraph库,可以分析引用

import objgraph
 
a = [1, 2, 3]
b = [4, 5, 6]
 
a.append(b)
b.append(a)
 
objgraph.show_refs([a])

写出对机器和阅读者友好的python代码

  • 对字典的遍历不要使用keys,因为keys会生成一个临时列表,导致多余的内存浪费并且运行缓慢,使用iterator。

  • is和==的正确使用

  • 不要使用import一次导入多个模块

合理地运用assert

assert 1==2, 'This should fail'

这个语句等价于

if __debug__:
    if not expression1: raise AssertionError(expression2)

这里的__debug__是一个常数。如果 Python 程序执行时附带了-O这个选项,比如Python test.py -O,那么程序中所有的 assert 语句都会失效,常数__debug__便为 False;反之__debug__则为 True。

巧用上下文管理器和With语句精简代码

在python中,使用上下文管理器帮助程序员自动分配并且释放资源,其中最典型的就是with语句。

with open('test.txt', 'w') as f:
        f.write('hello')
some_lock = threading.Lock()
with somelock:
    ...
自定义上下文管理器
基于类的上下文管理器
class FileManager:
    def __init__(self, name, mode):
        print('calling __init__ method')
        self.name = name
        self.mode = mode 
        self.file = None
        
    def __enter__(self):
        print('calling __enter__ method')
        self.file = open(self.name, self.mode)
        return self.file
 
 
    def __exit__(self, exc_type, exc_val, exc_tb):
        print('calling __exit__ method')
        if self.file:
            self.file.close()
            
with FileManager('test.txt', 'w') as f:
    print('ready to write to file')
    f.write('hello world')
    
## 输出
calling __init__ method
calling __enter__ method
ready to write to file
calling __exit__ method

如果在with内抛出了异常,exit可以接收到,如果你在exit内处理了异常,记得返回True,否则异常仍会继续抛出。

基于生成器的上下文管理器
from contextlib import contextmanager
 
@contextmanager
def file_manager(name, mode):
    try:
        f = open(name, mode)
        yield f
    finally:
        f.close()
        
with file_manager('test.txt', 'w') as f:
    f.write('hello world')

python的调试工具及性能分析工具

pdb使用例子

a = 1
b = 2
import pdb
pdb.set_trace()
c = 3
print(a + b + c)

cprofile使用例子

python3 -m cProfile xxx.py

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xqa86bez-1612074310430)(C:\Users\Jazon\AppData\Roaming\Typora\typora-user-images\1612073008527.png)]

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值