运行操作目录

假设有一个这样的文件夹结构:

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 语法块之前调用,返回值会赋值给 withtarget。就相当于语句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)

就这四个函数/方法进行分析解读:

  1. func: 这是一个普通的Python模块函数,不依赖于任何类或对象。

  2. func1: 这是一个类方法,使用@classmethod装饰器。

  3. func2: 这是一个静态方法,使用@staticmethod装饰器。

  4. 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)

接下来我们对它们进行性能上的排序(高到低):

  1. 模块函数:性能最强,理由如下:

    1. 简单性: 模块函数通常比较简单,不涉及复杂的对象状态和关系。这使得它们在执行时更加高效,因为不需要进行额外的对象创建和管理。

    2. 无状态: 模块函数不依赖于任何特定对象的状态,这意味着它们在执行时不会受到对象状态的影响。这使得它们在处理相同任务时具有一致的性能表现。

    3. 可重用性: 模块函数可以在程序中的多个地方重复使用,而不需要每次都创建新的对象实例。这种可重用性减少了对象创建和销毁的开销,提高了性能。

    4. 易于优化: 由于模块函数通常比较简单,编译器和解释器更容易对它们进行优化。优化后的代码运行速度更快,提高了性能。

    5. 独立性: 模块函数独立于特定对象实例,这意味着它们可以在不同的上下文和环境中使用,而不需要考虑对象之间的交互和依赖关系。这种独立性减少了程序中的耦合度,使代码更加清晰和易于维护。

  2. 类方法、静态方法:性能相似,因为它们都不依赖于对象的状态。它们之间的性能差异取决于Python解释器的实现和具体的代码优化。

  3. 实例方法:性能通常是最差的,因为它需要创建对象实例并在方法内部使用该实例的状态。

文章作者: Vsoapmac
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 soap的会员制餐厅
python 个人分享
喜欢就支持一下吧