python拾遗

这篇博客主要记录一些深入的python知识点
主要参考文章:
python官方文档
https://tenthousandmeters.com/blog/python-behind-the-scenes-11-how-the-python-import-system-works/

python的import系统

什么是module与module object

module是python的一个类型,在types模块中有定义,为types.ModuleType
几种文件可以作为module:

  • .py文件
  • .so文件
  • .pyc文件
  • .pyd文件
  • .zip文件
    (可以从sys.path查看有哪些路径包含这些文件)

package与submodule

module可以拥有submodule,拥有submodule的module称为package
所以package是一种特殊的module,它含有__path__属性,指向一个list,list中的元素是str,表示package的搜索路径

-m switch
solution and explanation
https://stackoverflow.com/questions/16981921/relative-imports-in-python-3

import的过程

import是对__import__函数的封装,__import__函数的参数是一个str,表示module的名字
? 为什么__import__是一个python函数,而不是一个c函数
__import__的具体过程:

  1. resolve the module name

可以echo “import math” | python -m dis
可以查看__import__的docstring看看它的参数是什么

circular import的原因和解决方法

python的local与global变量

在Doc的programming FAQ中提到

In Python, variables that are only referenced inside a function are implicitly
global. If a variable is assigned a value anywhere within the function’s body,
it’s assumed to be a local unless explicitly declared as global.
已经非常明确说明了如果函数内部有对该变量的赋值操作,那么该变量就是local变量,否则就默认global变量

与闭包的联系

由于闭包里可能会对外部变量进行引用,这时候就会涉及到local与global变量的问题有以下例子:

squares = []
for i in range(5):
    squares.append(lambda: i ** 2)
print(squares[0]()) # 16
print(squares[1]()) # 16

无论调用squares中的哪个函数,都会返回16,这是因为闭包中的i是对外部变量i的引用,当闭包被调用时才去访问这个变量,而此时外部变量i已经变成了4
如果想要得到正确的结果,可以使用默认参数的方式:

for i in range(5):
    squares.append(lambda x=i: x ** 2)

这样每次调用闭包时,都会创建一个新的变量x,而x的值是在闭包创建时就已经确定了

如何在多个module中共享变量

在python中,module是一个namespace,不同的module之间是相互独立的,如果想要在多个module中共享变量,可以使用import来实现,但是这样会导致循环import的问题,解决方法是将变量放在一个单独的module中,然后在需要使用的module中import这个module,一般这个module会被命名为config或者settings

# config.py
x = 1
y = 2
# module1.py
import config
config.x = 2
# main.py
import module1
import config
print(config.x)

python中的数字与字符串

向下取整

-22 // 10 = -3这与C语言不同,C语言中-22 / 10 = -2。这么做的原因是python中i % j的结果正负号与j相同,而C语言中i % j的结果正负号与i相同。因为python认为j取负数的情况很少,取正数的情况很多(比如现在是10点,问190个小时之前是几点,-190%12返回2会比返回-10好,表示从当前开始继续向正方向走两个小时)。如果要满足i%j的结果与j正负相同,且i == (i//j)*j+(i%j)那i//j的结果就必须向下取整,也就与C不一样

字符串与数字互转

python的int函数可以将字符串转为10进制数字,默认传入的字符串是10进制的,如果传入的字符串是16进制的,需要指定base参数,python中的16进制以0x开头,八进制以0o开头,二进制以0b开头

将数字转为字符串用f-string可以很方便的实现,f-string是python3.6引入的新特性,可以在字符串前加f来使用,里面可以使用{}来引用变量,也可以在{}中使用表达式来计算以及格式化输出

字符串是不可变对象

在python中字符串是不可变对象,主要原因有以下几点:

  1. 很多时候字符串会作为hash的key,如果字符串是可变的,那么hash值也会变化,这样就不能作为字典的key
  2. 字符串是不可变的,可以在多个线程中共享,不用担心线程安全问题
  3. 性能优化,由于字符串不可变,可以在内存中共享,减少内存占用

如果想要原地修改字符串可以使用io.StringIO,它是一个类文件对象,可以像文件一样读写,但是它是在内存中的,不会写入磁盘。也可以使用array.array,它是一个数组对象,可以存储任意类型的数据,但是它的元素必须是同一种类型,例子如下:

import array
# 'u'表示array的元素是unicode字符
a = array.array('u', 'hello')
a[0] = 'H'
print(a) # array('u', 'Hello')
a.tounicode() # 'Hello'

但常见的方法是将字符串转为list,然后修改,再转回字符串

a = list('hello')
a[0] = 'H'
''.join(a) # 'Hello'

性能

两个分析性能的工具是cProfile和timeit,cProfile是一个分析器,可以查看函数调用的次数和时间,timeit是一个计时器,可以查看代码的运行时间

序列(tuple与list)

tuple(seq)会将可迭代对象转化为一个tuple,如果seq本身就是一个tuple,那么就会返回seq本身,如果seq是一个字符串,那么就会返回一个包含字符串中每个字符的tuple。list(seq)类似.

删除重复元素

比较常见的方法是使用set,但是这样会改变元素的顺序,如果想要保持原来的顺序,可以使用OrderedDict

from collections import OrderedDict
def remove_duplicates(seq):
    return list(OrderedDict.fromkeys(seq))

多维数组

一般推荐使用numpy构建多维数组,如果想使用list的嵌套实现多维数组需要注意列表的深浅拷贝问题,下面是一个经典的例子:

a = [[0]*3]*3
# a = [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
a[0][0] = 1
# a = [[1, 0, 0], [1, 0, 0], [1, 0, 0]]

这是因为[0]*3会创建一个包含3个0的list,然后[[0]*3]*3会创建一个包含3个指向同一个list的list,也就是说list与*的运算是浅拷贝,正确的方法是使用列表生成式

a = [[0]*3 for _ in range(3)]

序列的+=操作

在python中+=会调用对象的__iadd__方法,如果对象没有实现__iadd__方法,那么就会调用__add__方法,然后将结果赋值给原对象。

a_tuple = ([],1)
a_tuple[0] += [1]
# TypeError: 'tuple' object does not support item assignment
# print(a_tuple) ([1], 1)

上面的错误是因为+=被解释为a_tuple[0] = a_tuple[0]._iadd([1]),而tuple是不可变对象所以a_tuple[0]被赋值时会报错,但a_tuple[0]._iadd([1])是可以执行的,所以虽然会报错,但是a_tuple的值已经被修改了

类与对象

描述器

在python的官方文档中howto中有一份专门介绍描述器的文档Descriptor HowTo Guide这篇文档由浅入深地使用例子介绍了描述器的作用与技巧,非常值得一读,其中对描述器的定义以及精要总结如下:

A descriptor is what we call any object that defines __get__(), __set__(), or __delete__().

Optionally, descriptors can have a __set_name__() method. This is only used in cases where a descriptor needs to know either the class where it was created or the name of class variable it was assigned to. (This method, if present, is called even if the class is not a descriptor.)

Descriptors get invoked by the dot operator during attribute lookup. If a descriptor is accessed indirectly with vars(some_class)[descriptor_name], the descriptor instance is returned without invoking it.

Descriptors only work when used as class variables. When put in instances, they have no effect.

The main motivation for descriptors is to provide a hook allowing objects stored in class variables to control what happens during attribute lookup.

Traditionally, the calling class controls what happens during lookup. Descriptors invert that relationship and allow the data being looked-up to have a say in the matter.

Descriptors are used throughout the language. It is how functions turn into bound methods. Common tools like classmethod(), staticmethod(), property(), and functools.cached_property() are all implemented as descriptors.

简单的说就是一个定义了__get__等方法的对象就可以被称为描述器,描述器的作用是控制属性的访问过程,原先对属性的访问过程完全由对象控制,而描述器可以拦截这个过程,并定义自己的行为,比如可以在访问属性时打印日志,或者在访问属性时进行类型检查等,下面是文档中的一个例子:

import os

class DirectorySize:

    def __get__(self, obj, objtype=None):
        return len(os.listdir(obj.dirname))

class Directory:

    size = DirectorySize()              # Descriptor instance

    def __init__(self, dirname):
        self.dirname = dirname          # Regular instance attribute
# 下面是运行结果
>>> s = Directory('songs')
>>> g = Directory('games')
>>> s.size                              # The songs directory has twenty files
20
>>> g.size                              # The games directory has three files
3
>>> os.remove('games/chess')            # Delete a game
>>> g.size                              # File count is automatically updated
2

首先说一下描述器的使用方式,描述器必须作为类属性而非实例属性时才能发挥作用,这是因为python的属性访问机制是先查找实例属性,然后查找类属性,最后访问__getattr__方法,如果将一个属性定义为实例属性,也就是类似直接在__init__中使用self.size = DirectorSize(),则会直接访问这个属性,即传统的对象完全控制属性访问过程,此时会直接返回描述器对象,而不会调用描述器的__get__方法。
访问的逻辑由object._getattribute_()控制,对应的cpython源码在objects/object.c里的PyObject_GetAttr方法,其对应的python逻辑如下:

def object_getattribute(obj, name):
    "Emulate PyObject_GenericGetAttr() in Objects/object.c"
    null = object()
    objtype = type(obj)
    cls_var = getattr(objtype, name, null)
    descr_get = getattr(type(cls_var), '__get__', null)
    if descr_get is not null:
        if (hasattr(type(cls_var), '__set__')
            or hasattr(type(cls_var), '__delete__')):
            return descr_get(cls_var, obj, objtype)     # data descriptor
    if hasattr(obj, '__dict__') and name in vars(obj):
        return vars(obj)[name]                          # instance variable
    if descr_get is not null:
        return descr_get(cls_var, obj, objtype)         # non-data descriptor
    if cls_var is not null:
        return cls_var                                  # class variable
    raise AttributeError(name)

待定


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2128099421@qq.com

×

喜欢就点赞,疼爱就打赏