python进阶知识大全(一)
运行操作目录
假设有一个这样的文件夹结构:
father1/
|-- folder1/
| |-- test03.py
|-- folder2/
| |-- __init__.py
| |-- utils.py
|-- test01.py
father2/
|-- test02.py
我们在使用test01调用utils时,可以直接写from folder2.utils import utils
,因为test01.py执行后,会将father1文件夹装入sys.path中,才能识别到folder2是python包。
但是,test02.py和test03.py在使用from folder2.utils import utils
时,会报查询不到包utils
的错误。
在上面例子中,我们可以知道,python在执行某模块时,会将该模块的父目录装入sys.path中。例如,test01.py在运行时,会将father1装入sys.path;test02.py在运行时,会将folder1装入sys.path;test01.py在运行时,会将father2装入sys.path。如果转入sys.path的文件夹中没有folder2,此时就没办法导入utils.py了。
所以,在test02.py和test03.py中需要手动将father1转入sys.path中,使用如下代码装入:
import sys
sys.path.append("father1")
相对路径偏移
如果我们使用test01.py调用utils.py时,想要识别folder1中的test03.py文件,假设我们在utils.py中这么写:
def read_data()
with open("../folder1/test03.py") as file:
return file.read()
然后再test01.py中调用utils.py:
from folder2 import utils
utils.read_data()
会发现报文件找不到的错误。
这是因为,虽然以utils.py来看,test03.py的路径是../folder1/test03.py
。但是调用的是test01.py后,test03.py的路径应该是./folder1/test03.py
在python中,执行哪个模块,使用相对路径就以哪个模块为基准。即使调用的是其他的模块,在模块中写相对路径时也要以运行的模块为基准编写相对路径。
反射
反射(Reflection) 允许运行中的 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性和方法。反射的具体作用如下:
反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。
也就是说,利用反射,我们可以获取以下程序:
Class 对象
类中的所有字段
类中的所有构造方法
类中的所有非构造方法
反射看起来像是破坏了封装性,但是反射是可以作为一个工具,用来帮助程序员实现本不可能实现的功能(perform operations which would otherwise be impossible)。很多程序架构,尤其是三方框架,无法保证自己的封装是完美的。如果没有反射,对于外部类的私有成员,我们将一筹莫展,所以我们有了反射这一后门,为程序设计提供了更大的灵活性。工具本身并没有错,关键在于如何正确地使用。
新建一个类,该类为测试python反射的对象:
class Student:
id = 1
name = "123"
def havePencil(self):
print("He has a pencil")
判断属性或方法是否存在
调用内置函数,返回值为bool(若存在则返回True):
hasattr(object,'attrName')
形参说明:
object
:对象,可以是实例化对象attrName
:字符串,用于判断对象是否存在该属性或者方法
例子:
from Student import Student
# student = Student()
has_bool = hasattr(Student,"id") # 是否存在该属性
has_bool = hasattr(Student,"havePencil") # 是否存在该方法
print(has_bool)
添加/设置属性
调用内置函数:
setattr(object,'attrName',value)
形参说明:
object
:对象,可以是实例化对象attrName
:字符串,对象的属性value
:对象属性的值
object中,传入的对象是具体的实例化对象,则改变的是该对象的属性,否则会改变整体的对象属性
例子:
from Student import Student
student1 = Student()
student2 = Student()
print(student2.id)
setattr(student2,'id',2) # 改变student2对象中的id属性
print(student2.id)
print(student1.id)
setattr(Student,'id',3) # 改变全部Student对象中的id属性
print(student1.id)
print(student2.id)
获取属性或方法
调用内置函数:
getattr(object,'attrName')
形参说明:
object
:对象,可以是实例化对象attrName
:字符串,用于获取该对象的属性或者方法
例子:
from types import FunctionType
from Student import Student
id = getattr(Student, "id") # 获取对象的属性
print(id)
havePencil = getattr(Student, "havePencil") # 获取对象的方法,返回FunctionType类型
print(havePencil(Student())) # 调用时加()并传入对象
print(type(havePencil) is FunctionType)
删除属性或方法
调用内置函数:
delattr(object,'attrName')
形参说明:
object
:对象,可以是实例化对象attrName
:字符串,用于删除该对象的属性或者方法
例子:
from Student import Student
student1 = Student()
print(student1.id)
delattr(student2,'id') # 删除student1对象中的id属性
print(student1.id)
拆包
调用列表,元组的内容时,除了使用for循环遍历里面的内容外,可以使用一些方法单独获取其中一些元素,例如:
lis = ['a', 'b', 'c']
tup = (1, 2, 3)
a, b, c = lis
A, B, C = tup
print(a) # a
print(b) # b
print(c) # c
print(A) # 1
print(B) # 2
print(C) # 3
这种行为可以理解为拆包,列表元组可以看作一个包,a, b, c = lis
这块地方相当于将包里面的元素一个一个拆解出来,单独使用。
字典也可以这么拆
dic = {'1': 'a', '2': 'b', '3': 'c'}
A, B, C = dic
print(A) # 1
print(B) # 2
print(C) # 3
print(dic[A]) # a
print(dic[B]) # b
print(dic[C]) # c
可以看到,字典拆包后拆出来的是key
星号的使用
星号可以对列表元组进行拆包,形式是这样:
lis = ['a', 'b', 'c']
tup = (1, 2, 3)
print(*lis)
print(*tup)
输出:
a b c
1 2 3
可以看到,其输出了类似于lis = a, b, c
的形式,利用这个原理,我们可以将其运用到函数上。
lis = ['a', 'b', 'c']
tup = (1, 2, 3)
def func(arg1, arg2, arg3):
print(arg1)
print(arg2)
print(arg3)
func(*lis) # 相当于func('a', 'b', 'c')
func(*tup) # 相当于func(1, 2, 3)
输出:
a
b
c
1
2
3
是不是很神奇?星号其实不仅仅运用在实参中,也可以运用在函数的形参里面。
在函数中,如果需要多个参数,但又不指定形参名字时,可以用*args
和**args
对函数的形参进行拆包
单个星号拆开的是元组的包,就是函数输入多个参数时,会把这些参数当作一个元组并拆开。例如:
def func(*args):
for arg in args:
print(arg)
func(1, 2, 3, 4, 5)
输出:
1
2
3
4
5
两个星号拆开的是字典的包,函数输入的多个参数必须是key=value
的形式,然后将这些形参当作一个字典拆开。注意拆开后输出的是key而不是value。例如:
def func(**args):
for arg in args:
print(arg)
print(args[arg])
print("\n")
func(name='apple', age='3', contry='china')
输出:
name
apple
age
3
contry
china
装饰器
Python装饰器是一种特殊的函数,它可以修改其他函数的行为。装饰器通常使用@语法,放在被修饰函数的定义之前。例如:
@decorator
def my_function():
pass
该行为等价于:
def decorator(my_function):
my_function()
def my_function():
pass
decorator(my_function)
要使用自定义装饰器,需要定义一个函数,该函数接受一个函数作为参数并返回一个新函数。例如:
def my_decorator(func):
def wrapper():
print("我执行了装饰器!!")
func()
print("装饰器执行完毕!!")
return wrapper
然后,可以使用@my_decorator来装饰my_function,然后运行my_function:
@my_decorator
def my_function():
print("Hello World!")
my_function()
需要注意的是装饰器返回的函数可能需要使用functools.wraps将原函数的元数据复制到装饰函数中,这样可以避免影响原函数的行为。
若出现两个装饰器,那么最顶层的装饰器会装饰下面的一层装饰器,例如:
@decorator1
@decorator2
def my_function():
print("Hello World!")
my_function()
decorator1
装饰器会装饰decorator2
装饰器,然后decorator2
装饰器会装饰my_function
函数,输出后如下:
我执行了装饰器!!
我执行了装饰器!!
Hello World!
装饰器执行完毕!!
装饰器执行完毕!!
如果需要传入参数,在my_function
中传入形参即可
def my_function(cls, func):
def func_wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result
return func_wrapper
上下文管理
所谓的上下文管理,其实是with
关键字背后的运行原理
在打开文件时,很多人通常直接用open('file')
。在文件读写时可以使用 with
关键字。优点是当子句体结束后文件会正确关闭,即使在某个时刻引发了异常。
with open('workfile') as f:
read_data = f.write("test")
其作用相当于:
f = open('workfile')
read_data = f.write("test")
f.close()
虽然with
非常方便,但是with
后面的表达式是不可以任意写的。要想使用 with
语法块,with
后面的的对象需要实现上下文管理器协议。
一个类在 Python 中,只要实现以下方法,就实现了上下文管理器协议:
__enter__
:在进入with
语法块之前调用,返回值会赋值给with
的target
。就相当于语句with open('workfile') as f
中的f
__exit__
:在退出with
语法块时调用,一般用作异常处理和数据清理
我们来看实现了这 2 个方法的例子:
class ExcelUtils:
book = None
sheet = None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if self.book is not None:
self.book.save() # 先保存 excel 后关闭,防止数据暴毙
self.book.close()
self.book = None
self.sheet = None
该方法实现了上下文管理器协议,具体调用的步骤如下:
with ExcelUtils() as eu:
eu.xxxxx()
上面的例子中,ExcelUtils
类的__enter__
函数返回了它自己本身,这样就可以在接下来的语句中调用该类的方法,__exit__
函数定义了excel应用的退出操作,这样即使程序报错,程序也能正常退出(不会锁住文件)。
如果在 with
语句块内发生了异常,那么 __exit__
方法可以拿到关于异常的详细信息:
exc_type
:异常类型exc_value
:异常对象exc_tb
:异常堆栈信息
输出重定向
在我们print时,它会打印在console中,如果我们想在print时打印到其他地方,我们就需要输出重定向
使用sys.stdout =
将输出重定向到其他地方。比如,输出到文件中:
import sys
sys.stdout = open("output.txt", 'w', encoding="UTF-8")
print("="*20 + " Start testing " + "="*20)
for i in range(200):
print(f"已迭代{i}代..........")
print("数据完毕")
print("="*20 + " End off testing " + "="*20)
sys.stdout = sys.__stdout__
print("恢复print输出位置")
结尾可以使用sys.stdout = sys.__stdout__
恢复print输出位置
python性能优化
列表推导式与for循环
虽然列表推导式的阅读性比for循环差,但列表推导式的性能是比for循环强的
for循环字符串拼接
我们经常会遇到for循环字符串拼接的情况,例如:
from time import time
start = time()
str_ = "abc_"
i = 0
while i < 100000:
str_ += str(i)
i += 1
str_ = ''.join(str_)
end = time()
print(round(end - start, 3))
此时str_
拼接的方法是str_ += str(i)
,运行后所耗时间为:
1.062 s
但如果用列表拼接并转换成string后,代码如下:
from time import time
start = time()
str_ = ["abc_"]
i = 0
while i < 100000:
str_.append(str(i))
i += 1
str_ = ''.join(str_)
end = time()
print(round(end - start, 3))
运行后所消耗时间为:
0.041 s
很明显str_ += str(i)
这种方式需要更多的时间去拼接字符串,而且随着循环的次数越大,所占的时间越多。
请尽可能用列表拼接的方式凭借字符串。
模块函数、类方法、静态方法与示例方法的性能分析
首先认识什么是模块函数、类方法、静态方法与示例方法,以下是一个示例:
from time import sleep
def func(time):
for _ in range(10):
sleep(time)
class class1:
@classmethod
def func1(cls, time):
for _ in range(10):
sleep(time)
class class2:
@staticmethod
def func2(time):
for _ in range(10):
sleep(time)
class class3:
def func3(self, time):
for _ in range(10):
sleep(time)
就这四个函数/方法进行分析解读:
func: 这是一个普通的Python模块函数,不依赖于任何类或对象。
func1: 这是一个类方法,使用
@classmethod
装饰器。func2: 这是一个静态方法,使用
@staticmethod
装饰器。func3: 这是一个实例方法,定义在类中但没有使用任何装饰器。
为了调用这四个函数/方法,我们需要用以下方法导入并调用(假设该模块命名为model1.py
,在包Utils
里面):
from Utils import model1
model1.func(1)
from Utils.model1 import class1
class1.func1(1)
from Utils.model1 import class2
class2.func2(1)
from Utils.model1 import class3
c3 = class3()
c3.func3(1)
接下来我们对它们进行性能上的排序(高到低):
模块函数:性能最强,理由如下:
简单性: 模块函数通常比较简单,不涉及复杂的对象状态和关系。这使得它们在执行时更加高效,因为不需要进行额外的对象创建和管理。
无状态: 模块函数不依赖于任何特定对象的状态,这意味着它们在执行时不会受到对象状态的影响。这使得它们在处理相同任务时具有一致的性能表现。
可重用性: 模块函数可以在程序中的多个地方重复使用,而不需要每次都创建新的对象实例。这种可重用性减少了对象创建和销毁的开销,提高了性能。
易于优化: 由于模块函数通常比较简单,编译器和解释器更容易对它们进行优化。优化后的代码运行速度更快,提高了性能。
独立性: 模块函数独立于特定对象实例,这意味着它们可以在不同的上下文和环境中使用,而不需要考虑对象之间的交互和依赖关系。这种独立性减少了程序中的耦合度,使代码更加清晰和易于维护。
类方法、静态方法:性能相似,因为它们都不依赖于对象的状态。它们之间的性能差异取决于Python解释器的实现和具体的代码优化。
实例方法:性能通常是最差的,因为它需要创建对象实例并在方法内部使用该实例的状态。