Advanced Python

1. 装饰器

内置装饰器

class A:
    b = 0
    def __init__(self):
        self.a = 1
    @classmethod
    def foo(cls, a):
        print(a)
    @classmethod
    def bar(cls, a):
        cls.b += a
        print(cls.b)
A.bar(3)
A.bar(2)

自定义装饰器

例子1:

def my_decorator(func):
    def wrapper_function(*args, **kwargs):
        print("*"*10)
        res = func(*args,  **kwargs)
        print("*"*10)
        return res
    return wrapper_function
@my_decorator
def foo(a):
    return a
# 相当于foo=my_decorator(foo)
x = foo(1)

例子2:

property 装饰器

例子来源于 Python 官方文档

根据前面所述,装饰器只是一个语法糖。property 函数的特征标(signature)如下:

前一段代码等价于这种直接使用 property 函数的做法:

备注:property 本质上是一个 Descriptor,参见后面。

2. 魔术方法与内置函数

2.0 Python 官方文档

  • 官方文档主目录:https://docs.python.org/3/

  • 对 Python 语言的一般性描述:https://docs.python.org/3/reference/index.html

    • 数据模型:https://docs.python.org/3/reference/datamodel.html

  • Python 标准库:https://docs.python.org/3/library/index.html

    • build-in functions(官方建议优先阅读此章节):https://docs.python.org/3/library/functions.html

    • build-in types:https://docs.python.org/3/library/stdtypes.html

  • Python HOWTOs(深入介绍一些主题,可以认为是官方博客):https://docs.python.org/3/howto/index.html

    • Descriptor HowTo Guide:https://docs.python.org/3/howto/descriptor.html

2.1 object 类

__module__

__weakref__

2.2 __str____repr__ 特殊方法,str、repr 内置函数

从设计理念上说:两者都是将对象输出,一般而言,__str__ 遵循可读性原则,__repr__ 遵循准确性原则。

分别对应于内置方法 strrepr,二者在默认情况(不重写方法的情况下)下都会输出类似于 <Classname object at 0x000001EA748D6DC8> 的信息.

备注: 在 jupyter notebook 中, 对 pandasDataFrame 使用 print 方法,打印出的结果不美观,但不用 print 却很美观,原因未知。

2.3 内置函数 vars 与 __dict__ 属性

从设计理念上说,vars 函数的作用是返回对象的属性名(不会包含方法及特殊属性)。__dict__ 属性里保存着对象的属性名(不会包含方法以及特殊属性)。这里的特殊属性指的是 __xxx__

一般情况下,Python 中的对象都有默认的 __dict__ 属性。而 vars(obj) 的作用就是获取对象 obj__dict__ 属性。关于 vars 函数的解释可以参考官方文档,如下:

Return the __dict__ attribute for a module, class, instance, or any other object with a __dict__ attribute.

Objects such as modules and instances have an updateable __dict__ attribute; however, other objects may have write restrictions on their __dict__ attributes (for example, classes use a types.MappingProxyType to prevent direct dictionary updates).

Without an argument, vars() acts like locals(). Note, the locals dictionary is only useful for reads since updates to the locals dictionary are ignored.

A TypeError exception is raised if an object is specified but it doesn’t have a __dict__ attribute (for example, if its class defines the __slots__ attribute).

备注:object 类没有 __dict__ 属性,但继承自 object 子类的对象会有一个默认的 __dict__ 属性(有一个例外是当该类定义了类属性 __slots__ 时,该类的对象就不会有 __dict__ 属性)。

__dict__ 属性与 Python 的查找顺序(lookup chain)息息相关,详情见 Descriptor

2.4 __slots__属性

从设计理念上说,__slots__ 属性的作用是规定一个类只能有那些属性,防止类的实例随意地动态添加属性。

可以定义类属性 __slots__(一个属性名列表),确保该类的实例不会添加 __slots__ 以外的属性。一个副作用是定义了 __slots__ 属性的类,其实例将不会拥有 __dict__ 属性。具体用法如下:

注意:假设类 B 继承自定义了 __slots__ 的类 A,那么子类 B 的实例不会受到父类 __slots__ 的限制。

2.5 内置函数 dir 与 __dir__ 方法

从设计理念上说:不同于 vars 与 __dict__,dir 方法倾向于给出全部信息:包括特殊方法名

dir 函数返回的是一个标识符名列表,逻辑是:首先寻找 __dir__ 函数的定义(object 类中有着默认的实现),若存在 __dir__ 函数,则返回 list(x.__dir__())。备注:__dir__ 函数必须定义为一个可迭代对象。

若该类没有自定义 __dir__ 函数,则使用 object 类的实现逻辑,大略如下:

If the object does not provide __dir__(), the function tries its best to gather information from the object’s __dict__ attribute, if defined, and from its type object. The resulting list is not necessarily complete, and may be inaccurate when the object has a custom __getattr__().

The default dir() mechanism behaves differently with different types of objects, as it attempts to produce the most relevant, rather than complete, information:

  • If the object is a module object, the list contains the names of the module’s attributes.

  • If the object is a type or class object, the list contains the names of its attributes, and recursively of the attributes of its bases.

  • Otherwise, the list contains the object’s attributes’ names, the names of its class’s attributes, and recursively of the attributes of its class’s base classes.

——https://docs.python.org/3/library/functions.html

备注:官方文档对默认的 dir 函数的实现逻辑有些含糊不清,只能简单理解为默认实现会去寻找 __dict__ 属性,故暂不予以深究。这里留一个测试例子待后续研究:

例子

输出结果为:(__getattribute____getattr__ 见下一部分,大体上是寻找了 __dict__ 属性与 __class__ 属性)

2.6 __getattr____getattribute__ 特殊方法,getattr 内置函数

从设计理念上说,这三者的作用是使用属性名获取属性值,也适用于方法

作用:__getattribute__ 会拦截所有对属性的获取。

首先内置函数 getattr(object, name[, default]) 的功能等同于 object.name,例如:getattr(a, "name") 等价于 a.name。实现细节上,内置函数 getattr 会首先调用 __getattribute__,如果找不到该属性,则去调用 __getattr__ 函数。

备注:object 类只有 __getattribute__ 的定义,而没有 __getattr__

备注:对于以双下划线开头的变量,编译时会对其名称进行修改:

备注:如果要自定义 __getattribute__ 函数,最好在其内部调用 object.__getattribute__(self, name)

以下通过一个例子说明清楚:

总结如下, 获取属性值的方法有如下几种:

  • obj.name: 最常见的形式, 这要求 name 必须是一个合法的标识符, 其执行逻辑是, 先进入 __getattribute__ 方法内, 如果触发 AttributeError, 就继续执行 __getattr__ 方法

  • getattr(obj, name): 执行逻辑与 obj.name 完全相同, 唯一的优势是 name 可以不是合法的标识符

这两种仅用于解释概念, 通常来说不会使用到

  • obj.__getattribute__(name): 它会首先触发一次 getattr("__getattribute__")(因此进入__getattribute__), 然后再进入 __getattribute__ 方法内, 但不会再进入 __getattr__ 方法内

  • obj.__getattr__(name): 它会首先触发一次 getattr("__getattr__") (因此进入__getattribute__), 然后在直接进入 __getattr__ 方法内

补充

  • hasattr(obj, name) 的执行逻辑是: 执行一次 getattr(obj, name), 如果触发 AttributeError, 那么就返回 False, 否则返回 True

2.7 delattr 内置方法、__delattr__ 特殊方法、del 语句、__del__ 特殊方法

作用:__delattr__ 会拦截所有对属性的删除。

分为两组, 第一组是删除对象, 参考官方文档

  • del obj: 引用计数减 1

  • obj.__del__(): 如果某个对象的引用计数为 0, 则触发此方法

示例

第二组是删除属性【待确认】

参考 Pytorch torch.nn.module__delattr__ 方法的实现, 应该也是一般要调用 object.__delattr__(self, name) 避免无限循环, 并且也通常会调用 del 语句来实现逻辑?

  • delattr(obj, name): 触发 __delattr__, name 可以不是标识符

  • obj.__delattr__(name): 触发一次 getattr(obj, "__delattr__"), 然后再执行 __delattr__, name 可以不是标识符

  • del obj.name: 参考第一组的解释

2.8 setattr 内置方法、__setattr__ 特殊方法

作用:__setattr__ 会拦截所有对属性的赋值。

参考链接 以及 pytorch 的 torch.nn.Module__setattr__ 的写法。

重载 __setattr__ 方法一般会调用 object.__setattr__(self, name, value) 避免无限循环, 下面是一个错误的例子:

总结如下: 以下几种方式给属性赋值:

  • obj.name=value: 直接触发 __setattr__ 方法, 但这里的 name 得是一个合法的标识符

  • setattr(obj, name, value): 同上, name 可以不是合法的标识符

这种方式仅做说明, 平时不会使用到

  • obj.__setattr__(name, value): 同上, 但会多触发一次 getattr("__setattr__") 的调用, name 可以不是合法的标识符

2.9 Descriptor、__get____set____delete__

参考:

注:大多数情况下,无须使用 Descriptor

概念

按照如下要求实现了 __get____set____delete__ 其中之一的类即满足 Descriptor 协议,称这样的类为 Descriptor(描述符) 。若没有实现 __set____delete__ 方法,称为 data descriptor,否则称为 non-data descriptor

Descriptor 的作用

在 Python 的底层,staticmethod()property()classmethod()__slots__ 都是借助 Descriptor 实现的。

def foo(self, *args) 可以使用 obj.foo(*args) 进行调用也是使用 Descriptor 实现的。

The starting point for descriptor invocation is a binding, a.x. How the arguments are assembled depends on a:

  • Direct Call

    The simplest and least common call is when user code directly invokes a descriptor method: x.__get__(a).

  • Instance Binding

    If binding to an object instance, a.x is transformed into the call: type(a).__dict__['x'].__get__(a, type(a)).

  • Class Binding

    If binding to a class, A.x is transformed into the call: A.__dict__['x'].__get__(None, A).

  • Super Binding

    If a is an instance of super, then the binding super(B, obj).m() searches obj.__class__.__mro__ for the base class A immediately preceding B and then invokes the descriptor with the call: A.__dict__['m'].__get__(obj, obj.__class__).

—— https://docs.python.org/3/reference/datamodel.html#invoking-descriptors

查找顺序

完整的顺序如下,对于 obj.x,获得其值的查找顺序为(参考Realpython):

  • 首先寻找命名为 xdata descriptor。即如果在 obj 的类 Obj 定义里有如下形式:

    其中 DescriptorTemplate 中定义了 __set____del__ 方法。

  • 若上一条失败,在对象 obj__dict__ 属性中查找 "x"

  • 若上一条失败,寻找命名为 xnon-data descriptor。即如果在 obj 的类 Obj 定义里有如下形式:

    其中 DescriptorTemplate 中定义了 __get__ 但没有定义 __set____del__ 方法。

  • 若上一条失败,则在 obj 类型的 __dict__ 属性中查找,即 type(obj).__dict__

  • 若上一条失败,则在其父类中查找,即 type(obj).__base__.__dict__

  • 若上一条失败,则按照父类搜索顺序 type(obj).__mro__,对类祖先的 __dict__ 属性依次查找。

  • 若上一条失败,则得到 AttributeError 异常。

例子:

如果类没有定义 __slot__ 属性及 __getattr__ 方法,且 __getattribute____delattr____setattr__ 这些方法都直接继承自 object 类,那么 __dict__ 的构建将会是如下默认的方式:

查找顺序

使用 Descriptor

需实现下列函数,实现 __get____set____delete__ 其中之一即可,__set_name__ 为 Python 3.6 引入的新特性,可选。参照例子解释:

例子

实用例子

避免重复使用 property

可以使用如下方法实现

2.10 pickle 与 __setstate____getstate__ 方法

某些时候,一个对象无法进行序列化,则可以自定义 __getstate__,在进行序列化时,只序列化 __setstate__ 的返回值。另外,可自定义 __setstate__ 方法,在反序列化时,利用 __getstate__ 的返回值将对象恢复。具体可参考官方文档

一个说明功能的例子:

更有意义的例子待补充

3. 继承

MRO (Method Resolution Order) 与 C3 算法

Python 在产生多继承关系时,由于子类可能有多个或多层父类,因此方法的搜索顺序(MRO, Method Resolution Order)很重要,同时,搜索顺序也涉及到类的属性。对于属性或者变量的访问,按照 MRO 的顺序依次搜索,直到找到匹配的属性或变量为止。对于每个类,可以使用如下代码来获取 MRO :

本部分参考 C3 算法官方文档

unless you make strong use of multiple inheritance and you have non-trivial hierarchies, you don't need to understand the C3 algorithm, and you can easily skip this paper.

一点历史与 MRO 应满足的性质

在 Python 的历史上,曾出现了若干种 MRO 算法,自 Python 2.3 以后,使用 C3 算法,它满足两个性质(之前的算法违背了这两个性质,所以可能会引发隐蔽的 BUG)

  • local precedence ordering:MRO 的结果里应该保证父类列表的相对顺序不变。例如:

    MRO(A) 序列必须为 [A, ..., B, ..., C, ..., D, ...] 这种形式。

  • monotonicity(单调性):如果 C 的 MRO 序列中 A 排在 B 的前面,那么对于任意继承自 C 的类 D,D 的 MRO 序列中 A 也排在 B 的前面

C3 算法

引入记号:

  • B1B2...BnB_1B_2...B_n 代表 [B1,B2,...,Bn][B_1,B_2,...,B_n]。用 C+B1...BnC+B_1...B_n 代表 CB1,...BnCB_1,...B_n。即类 CC 的 MRO 序列为 L(C)L(C)

  • 对于序列 B1...BnB_1...B_nB1B_1 称为头,B2...BnB_2...B_n 称为尾

C3 算法描述为:

其中 merge 的规则为:

递归调用 merge 操作:

记第一个序列中的头为 HH,若 $H$ 不在其余任意序列的尾中,则将 HH 添加到 MRO 序列中,并对 merge 中的所有序列中删除 HH,之后对剩余序列继续 merge 操作;否则对第二个序列的头进行上述操作,直至最后一个序列。若直到最后一个序列都无法进行删除操作,那么判定为继承关系不合法。

例子:

super 函数

参考资料:RealPython、《Python Cookbook (3ed)》chapter 8.7。

由于方法覆盖的特性,以方法为例,如果类的 MRO 顺序中有同名方法,那么处于 MRO 靠后类的同名方法将会被隐藏。因此如果需要调用父类被隐藏的方法,需要对 MRO 顺序进行调整。这就是 super 方法的作用。

super 函数有两种调用形式

  • 两个参数的形式:super(cls, obj)。其中第一个参数为子类,obj 为子类对象(也可以是子类的子类对象,但基本不可能会这样去用)。

  • 无参数形式:super()。推荐使用

super 实际上是一个类,但注意 super() 返回的不是父类对象,而是一个代理对象。

上例为典型的菱形继承方式,使用 super 可以按照 MRO 顺序依次调用 __init__ 函数一次。

备注:super 函数还有单参数的调用形式,参见 stckoverflow(理解需要有许多前置知识)。

4. 元类

参考资料:RealPythonPython 官方文档

类是用来构造实例的,因此类也可以被叫做实例工厂;同样地,也有构造类的东西,被称为元类。实际上每个类都需要用元类来构造,默认的元类为 type

类继承的写法

定义类的继承关系时的完整格式如下

这里位置参数 BC 是父类, 关键字参数 metaclass=D 是元类, 默认情况下 D=type, 而其余关键字参数 x=1, y=2 会被 B 的 __init_subclass__ 所使用到

type 函数

Python 中, type 函数是一个特殊的函数,调用形式有两种:

  • type(obj):返回 obj 的类型

  • type(name, bases, dict, **kwds): 用于创建一个类, 其中 bases 是父类元组, dict类属性, kwds 与元类有关, 疑问见下面

关于 kwds 的问题 (参考后面几节回过来再看):

  • 使用 type("C", (A,), {"a": 1}, extra=1) 会报错, 除非 A 定义了 __init_subclass__, 并且能处理 extra 参数

  • 可以使用 class C(metaclass=M, extra=1): 其中 M 继承自 type, 并且 M 重载 __new__ 方法, 其中 metaclass 是固定的变量名, 而 extra 是自定义的变量名, 被 M.__new__ 方法中使用到

  • 这个怎么做到的? https://docs.pydantic.dev/1.10/usage/model_config/

    部分解释

metaclass 与 __init_subclass__

参考 https://duongnt.com/init_subclass-metaclass/, 原博客写得更好, 这里摘录的内容不完全达意.

使用元类: 所谓元类, 是指继承自 type 的类, 并且重载了 type__new__ 方法, 注意 type.__new__object.__new__ 的区别

另一种做法是不使用元类, 而是在父类中定义 __init_subclass__, 子类只需要继承即可完成

type(...) vs type.__new__(...)

type(...)type.__new__(...) 仅有一些小区别: 调用 type(...) 会在内部调用 type.__new__, 然后进一步调用 type.__init__

https://stackoverflow.com/questions/2608708/what-is-the-difference-between-type-and-type-new-in-python

这个例子看上去与上面的描述矛盾, 实际上, 在第一种写法里, A 的定义完成时, 先触发 MetaA.__new__, 它在内部触发 type(...), 也就是会进一步调用 type.__new__type.__init__, 而这两步都没有输出; 在第二种写法里, A 的定义完成时, 先触发 MetaA.__new__, 由于其返回是用 type.__new__(...) 调用的, 因此会进一步触发 Meta.__init__ (类似于下面的 object.__new__object.__init__).

object.__new__ 函数与 object.__init__ 函数

以下是一个代码样例:

输出结果

abc 模块

最佳实践

  • stackoverflow: abstractmethod的函数体什么都不要写, 只包含 docstring 即可

  • stackoverflow: 继承自 ABC 或者使用 ABCMeta 没有本质区别,但似乎更推荐继承的方式,更简单。

abc 模块最常见是搭配使用 ABCMetaabstractmethod。其作用是让子类必须重写父类用 abstractmethod 装饰的方法,否则在创建子类对象时就会报错。参考

用法如下:

注意:不设定 metaclass=ABCMeta 时,abstractmethod 不起作用,即不会强制子类继承。

使用 ABCMetaabstractmethod 优于这种写法:

pydantic.v1.BaseModel

5. with语法(含少量contextlib包的笔记)

主要是为了理解pytorch以及tensorflow中各种with语句

主要参考链接

5.1 读写文件的例子

首先厘清读写文件的一些细节

以下三段代码中

  • 代码1如果在write时报错, 那么文件无法被close, 有可能引发BUG

  • 代码2保证文件会被close, 另外可以通过增加except语句, 使得可以处理各类异常

  • 代码3则相对优雅, 并且与代码2功能一致, 即使write出错, close依旧会被调用

代码3是怎么做到的呢? 其实际上基本等效于

注意到一般情况下, 此处的foo与file是不一样的对象, 参见下节中关于__enter__方法的返回值. 但在文件读写的情形下, foo与file是相同的对象. 另外, __exit__函数有三个参数, 在自定义这个函数时也应该遵循三个参数的设计(具体可以参考这个问答).

5.2 with语法与怎么让自定义类支持with语法

This interface of __enter__() and __exit__() methods which provides the support of with statement in user defined objects is called Context Manager.

总的来说, 需要让类支持with语法, 只需要定义魔术方法__enter____exit__即可, 一个完整的例子如下

5.3 使用contextlib包中的函数来使得类支持with语法

按照上一节的做法, 可以使用如下写法让MassageWriter支持with语法

也可以使用contextlib中的一些方法不进行显式定义__enter____exit__使得自定义类能支持with语法, 例子如下

执行顺序为:首先 open_file 函数被调用,并且将返回值 file 传递给 my_file,之后执行 with 语句内部的write 方法, 之后再回到 open_file 方法的 yeild file 后继续执行。可以简单理解为:

  • open_file函数从第一个语句直到第一个yield语句为__enter__

  • open_file函数从第一个yield语句到最后为__exit__

备注: 这里 try ... finally ... 的写法是典型写法:

5.4 "复合"with语句

6. for else语法

7. python基本数据类型

int: 无限精度整数

float: 通常利用C里的double来实现

8. 函数的参数

参考知乎

函数调用

使用了 a=x 这种方式传参的即为关键字实参。

两个具有一般形式的例子

函数定义

备注:限定位置形参在 Python 3.8 才被正式引入,即 / 这种写法。在此之前仅有后面的四种形参

一个具有一般形式的例子:

  • ab 为限定位置形参

  • cd 为普通形参

  • ef 为限定关键字形参

验证方式:

形实结合的具体过程

首先用位置实参依次匹配限定位置形参和普通形参,其中位置实参的个数必须大于等于限定位置形参的个数,剩余的位置实参依顺序匹配普通形参。

  • 若位置实参匹配完全部限定位置形参和普通形参后还有剩余,则将剩余参数放入 args

  • 若位置实参匹配不能匹配完全部普通形参,则未匹配上的普通形参留待后续处理

接下来用关键字实参匹配普通形参和限定关键字形参,匹配方式按参数名匹配即可。

设定默认值的规则

为形参设定默认值的规则与前面的规则是独立的。

  • 限定关键字形参,带默认值与不带默认值的形参顺序随意

  • 限定位置形参和普通形参,带默认值的形参必须位于不带默认值的形参之后

9. 导包规则

参考:

首先,需要厘清几个概念:

  • namespace

    这里的 a 是一个 namespace

  • module

    单个 .py 文件是一个 module

  • package

    目录,且目录下有 __init__.py 文件

  • namespace package

    目录,且目录下没有 __init__.py 文件

9.1 namespace

  • built-in namespace (运行脚本里的变量)

  • global namespace

  • enclosing namespace (带有内层函数的函数)

  • local namespace (函数最里面的一层)

global namespace 需要额外进行说明,与 import 相关。

9.2 import 语法详解

绝对导入与相对导入

from ... import ... 语法详解

下面分别对上述导入语句作解析:

导入成功只能为三种情况

  • aa 是一个不带 __init__.py 的文件夹(namespace package)。

    • bb 是一个 bb.py 文件。则可以直接使用 bb,但不能使用 aa 以及 aa.bb。注意,此时

    • bb 是一个带或者不带 __init__.py 的文件夹,情况类似,唯一的区别是此时 bb 会显示为一个 module 或者是 namespace。

  • aa 是一个带有 __init__.py 的文件夹(package),则上述导入成功的条件为 bbaa/__init__.py 中是一个标识符,或者 bbaa 的子目录,或者 bb.py 在文件夹 aa 下。无论是哪种情况,aa/__init__.py 均会被执行,且 aaaa.bb 不可直接使用。下面是一个例子:

    目录结构为

    文件内容为

    使用

  • aa 是一个 aa.py 文件,则上述导入成功的条件为 aa.py 中可以使用 bb 这一标识符。

结论:对于这种形式的导入

xx.pyxx/__init__.py 只要有就会被执行。并且 xxyy 是 namespace package 还是 package 不影响导入,最终只有 import 后面的东西可以直接使用。

import ... 语法详解

导入成功只能为一种情况 aa/bb/cc.py 或着 aa/bb/cc 存在,作用是依次执行 aa/__init__.pyaa/bb/__init__.pyaa/bb/cc.__init__.py (若它们都是package)。无论 aabb 是 package/namespace package,以下标识符均可以直接使用:

以下不可使用

备注:无论是 from ... import ... 还是 import ...,相关包的 __init__.pyxx.py 模块均会被执行一次。后续若再次 import,无论文件是否发生变动,均不会再次运行 __init__.pyxx.py 文件。只是标识符是否可用发生变化。

彻底理解import

step1:官方文档搜索记录

平时惯用的 import 语法是 importlib.__import__ 函数的语法糖:

The __import__() function

​ The import statement is syntactic sugar for this function ——https://docs.python.org/3/library/importlib.html

其函数定义为(链接):

官方对此函数的解释为:

An implementation of the built-in __import__() function.

Note: Programmatic importing of modules should use import_module() instead of this function.

即:importlib.__import__ 是内置函数的一种实现。备注:此处官方的超链接疑似有误,似乎应该是:平时惯用的 import 语法是内置函数 __import__ 函数的语法糖

而内置函数 __import__ 的定义为(链接):

官方对此函数有如下注解:

This function is invoked by the import statement. It can be replaced (by importing the builtins module and assigning to builtins.__import__) in order to change semantics of the import statement, but doing so is strongly discouraged as it is usually simpler to use import hooks (see PEP 302) to attain the same goals and does not cause issues with code which assumes the default import implementation is in use. Direct use of __import__() is also discouraged in favor of importlib.import_module().

可以看到,importlib.__import__ 与内置函数 __import__ 的定义完全相同。

总结:平时所用的 import 语句仅仅是 importlib.__import__ 函数(也许是内置函数 __import__)的语法糖。而 importlib.__import__ 是内置函数 __import__ 的一种实现,建议不要直接使用 importlib.__import__ 与内置的 __import__ 函数。

整理一下官方说明链接:

由于 importlib.__import__ 函数几乎没有任何说明,因此主要看链接 1 与 3。

step 2:官方文档理解

首先,回顾内置函数 __import__ 的定义:

在标准实现中,locals 参数被忽略。import 语法糖与 __import__ 内置函数的对应关系为:

官方文档的三个例子

晦涩难懂,之后再补充。

Python 导包的常用方法有:import 语句、__import__ 内置函数、importlib 模块。本质上讲,第一种方法实际上会调用第二种方法,而第三种方法会绕过第二种方法,一般而言不推荐直接使用第二种方法。

import 语句与 __import__ 内置函数的对应关系可以参见官方文档

怎样完全删除一个已经被导入的包,似乎做不到,参考链接

怎样实现自动检测包被修改过或未被导入过,自动进行 reload 操作:待研究

一些疑难杂症:

实例1:

想获得两个包中的模型实例,将两个模型串联进行推断

用于替代

实例2:

假定目录结构为:

文件内容如下:

运行:

报错:

原因在于 torch.hub.load 的内部逻辑为:

  • 按照 facebookresearch/detr:main 去 GitHub 下载原始仓库(https://github.com/facebookresearch/detr)的代码至 ~/.cache/torch/hub 下。

    备注:此处的 main 代表 main 分支,代码下载解压完毕后,~/.cache/torch/hub 目录下会生成子目录 facebookresearch_detr_main 存放当前分支下的代码

    备注:如果原始 GitHub 仓库进行了更新,而本地之前已经下载了之前版本的仓库,可以使用如下方法重新下载

  • 接下来使用动态 import 的方式,增加了 ~/.cache/torch/hub/facebookresearch_detr_main 到 sys.path 并使用 importlib 中的相关函数导入代码仓库顶级目录中的 hubconf.py 文件里的 detr_resnet50 函数,构建模型并下载权重。随后在 sys.path 中移除了 ~/.cache/torch/hub/facebookresearch_detr_main 路径。

问题出现在上述仓库的 hubconf.py 文件里有这种 import 语句:

导致当前目录下的 models 无法被重新导入

修改策略(未必万无一失):

10. Python buid-in fuction and operation

参考资料:Python 标准库官方文档

Truth Value Testing

任何对象都可以进行 Truth Value Testing(真值测试),即用于 bool(x)ifwhile 语句,具体测试流程为,首先查找该对象是否有 __bool__ 方法,若存在,则返回 bool(x) 的结果。然后再查找是否有 __len__ 方法,若存在,则返回 len(x)!=0 的结果。若上述两个方法都不存在,则返回 True

备注:__bool__ 方法应返回 True 或者 False__len__ 方法应返回大于等于 0 的整数。若不遵循这些约定,那么在使用 bool(x)len(x) 时会报错。相当于:

备注:__len__ 只有被定义了之后,len 方法才可以使用,否则会报错

boolean operation: or, and, not

运算优先级:非bool运算 > not > and > or,所以 not a == b 等价于 not (a == b)

注意这三个运算符的准确含义如下:

delattr function and del operation

11. Python 内存管理与垃圾回收(待补充)

12. 怎么运行 Python 脚本

主要参考(翻译)自:RealPython

主要有:

  • python xx/yy.py

  • python -m xx.yy

  • import

  • runpy

  • importlib

  • exec

13. 迭代器与生成器

这篇文章 的最后有一个使用迭代器推导式求一个大型 csv 文件某列和的代码, 适用于大文件, 很值得体会:

generator 高级用法: send, throw, close

参考资料: https://realpython.com/introduction-to-python-generators/

generator 还有着三个方法 send, throw, close.

send

例子参考: https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/

执行逻辑为:

  • 第一个 next(iterator) 会执行到 yield 处,返回结果为 0

  • 接下来的 send(2) 会将 2 传递给 jump,然后再次执行至 yield 处,返回结果为 2

  • ...

备注:

  • next 实际上等同于 send(None)

  • 不能去掉第一个 next 直接执行 send(2),会报错 (可以使用 send(None))

close

close 方法用于关闭迭代器

throw

  • throw 的执行逻辑是在 yield 处触发异常, 然后执行到下一次 yield. 如果生成器函数不像上面这个例子中那样捕获异常并处理, 则上面代码将直接报错

yield from 关键字

python 中还有一个关键字 yield from, 虽然在简单场景下, yield from it 似乎跟 for i in it: yield i 没太大区别, 但实际上, 在 send, close, throw 方法上, 还是有区别的, 参考这个问答, 这里仅举一例:

执行结果

如果不使用 yield from, 那么执行结果将是:

引用上面这个问答的理解:

What yield from does is it establishes a transparent bidirectional connection between the caller and the sub-generator

在上面这个例子里:

  • sub-generator 指的是 w

  • caller 指的是 wrap.send(), 注意这个例子是对 wrap 调用 send

  • bidirectional 指的是 generator 的特性: 即可以 caller 可以通过 yield 拿到 generator 的结果, 也可以通过 send 改变 generator 的行为

14. 写一个 Python 包

以下内容将在未来全部删除, 请参考并以博客内容为准:

https://buxianchen.github.io/drafts/2024-04-16-python-package-manager.html

github

对照几份源码进行学习

  • pip:

  • numpy: https://github.com/numpy/numpy

  • pytorch: https://github.com/pytorch/pytorch

可以使用 git clone <url>.git 的方式克隆源代码,这里的 url 的形式为 https://github.com/<username or groupname>/<projectname>。而以 numpy 为例,其简化版目录结构如下:

大体上讲,平时使用 pip install numpy 实际发生的事情是,将此处的目录 numpy 放到 site-packages 目录下,而其余的 doc 目录的内容将不会被安装。

一个最简的例子

目录结构如下

项目组织形式

参考 stackoverflow,推荐以类似这种形式组织,注意这些 __init__.py 文件是必须的,以确保

安装方式为:

特别说明:关于测试数据与测试代码文件:以下为个人理解,不一定为最佳实践,测试代码中读取数据时应该要获取完整的路径,可以考虑使用 __file__ 结合相对路径以获取绝对路径。关于这一点,有如下的一个源码分析案例:

源码分析:参考 scikit-image 的源代码

其中,data.camera 函数的定义位于 skimage/data/__init__.py,它进一步调用了同文件下的 _load("data/camera.png"),而 _load 函数又调用了同文件下的 _fetch("data/camera.png"),而 _fetch 函数的关键代码如下:

例子:

项目

foo/main.py

foo/__init__.py 内容为空

data/data.txt

setup.py

安装与使用

备注:安装在 site-packages 目录下

安装依赖包

第一步:获取requirements.txt

方法一: 只获取必要的包(推荐使用)

方法二: 获取当前环境下的所有包

此方案尽量避免使用, 或者在一个干净的虚拟环境下使用

第二步:利用requirements.txt安装依赖包

项目打包详解

问题引出:

一些历史, 关于distutils, distutils2, setuptools等, 参考链接. 大体来说, distutils是最原始的打包工具, 是Python标准库的一部分. 而setuptools是一个第三方库, 在setuptools的变迁过程中, 曾出现过一个分支distribute, 现在已经合并回setuptools, 而distutils2希望充分利用前述三者:distutils, setuptools, distribute的优点成为标准库的一部分, 但没有成功, 并且已经不再维护了. 总之, distutils是标准库, setuptools是开发者常用的第三方库, 安装好后还额外带着一个叫easy_install的第三方管理工具, 而easy_install目前用的比较少, pip是其改进版. 顺带提一句: python源码安装一般是下载一个压缩包(先解压, 再编译, 再安装), 二进制安装一般是下载一个.egg或者.whl的二进制文件进行安装, 后者已经取代前者成为现今的通用标准. 下面仅介绍基于setuptools的使用, 其关键在于编写setup.py. 上传到PyPI的方法参考python官方文档.

pip install vs python setup.py install

一般来说,参考stackoverflow,推荐使用 pip install.

  • pip 会自动安装依赖包,使用 setup.py 通常需要手动安装。(此条存疑)解释:使用 pip 安装时一般只需要 pip install <PACKAGE_NAME> 即可,而 setup.py 通常需要 pip install -r requirements.txt & python setup.py install

  • pip 会自动追踪包的 metadata, 所以在卸载包时可以使用 pip uninstall <PACKAGE_NAME>,但是使用 setup.py 需要手动卸载再升级

  • pip 可以不需要手动下载:pip install xx(PyPi),pip install git+https://github.com/xxx/xxx.git(github/gitlab/...),或者对压缩包或whl文件安装:pip install xx.tar.gzpip install xx.whl。而 setup.py 只能下载并解压后才能安装

备注

对同一个包的安装混用 pip 与 setup 有时会出现一些难以解决的 bug。

pip
setup

pip install .

python setup.py install

pip install -e .

python setup.py develop

setup.py 的编写与使用简介

首先尝鲜,在介绍各个参数的用法(完整列表参见官方文档

setup.py 的 setup 函数的各个参数详解

xx_requires

entry_points 参数

指定这组参数后,例如:"labelme=labelme.__main__:main" 这一行表示执行完安装命令后,与可执行文件 python 同级的目录下会出现可执行文件 labelme,如果执行该文件,则等同于执行 labelme.__main__.py 文件内的 main 函数。

scripts 参数

似乎不推荐使用

其他参数

参数
含义

zip_safe

设置为False表示以文件夹的形式安装(方便调试), 设置为True表示安装形式为一个.egg压缩包

已经弃用的参数

已弃用的参数
替代品
含义

requires

install_requires

指定依赖包

data_files

package_data

指定哪些数据需要一并安装

将非代码文件加入到安装包中,注意:这些非代码文件需要放在某个包(即packages 列表)下,使用以下两种方式之一即可

  • 使用MANIFEST.in文件(放在与setup.py同级目录下), 并且设置include_package_data=True, 可以将非代码文件一起安装

  • package_data参数的形式的例子为:{"package_name":["*.txt", "*.png"]}

例子 1

labelme-4.5.12 的源码目录如下:

其中 labelme 文件夹内部的文件目录为:

setup 函数如下

使用 python setup.py install 后,安装相关的存储路径(conda)例如

Scripts 目录下多出了

打包方式最佳实践

目前主流的打包格式为 whl 格式(取代 egg 格式),发布到 PyPi 的包一般使用下面的命令进行安装

实际过程为按照包名 <packagename> 在互联网上搜索相应的 .whl 文件,然后进行安装。因此对于源码安装的最佳实践也沿用上述过程,详述如下:

setup.py 文件的 setup 函数的参数 packages 列表长度最好刚好为 1,此时 setup.py 文件的 setup 函数的参数 name 应与 packages 的唯一元素相同,且命名全部用小写与下划线,且尽量不要出现下划线。使用下面两条命令安装

在 site-packages 目录下会出现类似于如下两个目录

备注:whl 格式实际上是 zip 格式,因此可以进行解压缩查看内容

发布到 PyPi

参考资料

15. ...在python中的作用

... 在 python 中是一个对象, 等同于 Ellipsis。是一个单例模式的对象, 它没有任何方法.

常见的作用参考博客

16. 变量的作用域

更多关于作用域相关的内容可以辩证地参考: https://realpython.com/python-scope-legb-rule/

简单来说优先顺序就是: local scope, enclosing scope, global scope, buildin scope.

这个问题是 Python FAQ, Python 官方文档(executionmodel) 中也有这样的解释

If a name binding operation occurs anywhere within a code block, all uses of the name within the block are treated as references to the current block. This can lead to errors when a name is used within a block before it is bound. This rule is subtle. Python lacks declarations and allows name binding operations to occur anywhere within a code block. The local variables of a code block can be determined by scanning the entire text of the block for name binding operations. See the FAQ entry on UnboundLocalError for examples.

这里是解释器先看了整个 code block, 即先看了 print(var) 之后的 var=200 这条语句, 认为 var 应该是一个 local variable, 所以在真正执行时按从上到下, 在执行 print(var) 时发现局部变量 var 没有被定义, 引发报错

Python 中的变量类型一共只有 3 种: (引用自 Python 官方文档)

If a name is bound in a block, it is a local variable of that block, unless declared as nonlocal or global. If a name is bound at the module level, it is a global variable. (The variables of the module code block are local and global.) If a variable is used in a code block but not defined there, it is a free variable.

  • local variable:

  • global variable:

  • free variable:

一个关于 free variable 的例子:

这里的 outer_func 被称为 inner_funcenclosing function, 而 inner_func 被称为 outer_funcinner function (nested function). 从 inner_func 的视角看, who 变量是 free variable, 从 outer_func 的视角看, who 变量是 local variable

17. Closure

一篇博客: https://realpython.com/inner-functions-what-are-they-good-for/

closure 的在 wiki 上的 定义: (Python 中也沿用这些定义)

Operationally, a closure is a record storing a function together with an environment. The environment is a mapping associating each free variable of the function (variables that are used locally, but defined in an enclosing scope) with the value or reference to which the name was bound when the closure was created.

注意这里的 free variable, used locally, enclosing scope 都是站在 inner function 的视角来看待的, 简单来说:

closure 包含 inner function 和它的 free variable

closure 在被调用时的特点如下:

Unlike a plain function, a closure allows the function to access those captured variables through the closure's copies of their values or references, even when the function is invoked outside their scope.

这个例子中外层函数 generate_power (enclosing function) 的用途是一个 closure factory function, 而外层函数被调用后地返回值 raise_tworaise_three 被称为 closure, 我们可以看到: closure (在这个例子中是 raise_tworaise three) 的特点是它能被其它函数 (这个例子中是 generate_power) 动态地创建.

func.__closure__[0].cell_contentsfunc.__code__.co_freevarsfunc.__code__.co_cellvars 与闭包相关, 具体如下:

18. __code__

深入理解 Python 虚拟机inspect 模块: 包含 __code__ 的全部属性的解释

前面的第 8 节已经解释过: python 函数定义

__code__ 的全部属性

  • co_argcount

  • co_code (待研究)

  • co_cellvars: tuple of names of cell variables (referenced by containing scopes), 以闭包函数为例, 外层函数的 co_cellvars 是内层函数所使用的外层函数的变量

  • co_consts (待研究)

  • co_filename (待研究)

  • co_firstlineno (待研究)

  • co_flags (待研究)

  • co_lnotab (待研究)

  • co_freevars

  • co_posonlyargcount

  • co_kwonlyargcount

  • co_name (待研究)

  • co_qualname (待研究)

  • co_names (待研究): tuple of names other than arguments and function locals

  • co_nlocals: number of local variables, 实际上就是 len(co_varnames)

  • co_stacksize (待研究): virtual machine stack space required

  • co_varnames: tuple of names of arguments and local variables, 具体顺序是:【pos-only】,【pos】, 【keyword-only】, args, kwargs, 然后其余局部变量按使用顺序排列

附录 1

骚操作

来源:torch/cuda/amp/grad_scaler.py,GradScaler:scale 函数

不能实例化的类

python dict与OrderedDict

关于python自带的字典数据结构, 实现上大致为(参考stackoverflow回答):

  • 哈希表(开放定址法: 每个位置只存一个元素, 若产生碰撞, 则试探下一个位置是否可以放下)

  • python 3.6以后自带的字典也是有序的了(dict vs OrderedDict)

说明: 这里的顺序是按照key被插入的顺序决定的, 举例

深复制/浅复制/引用赋值

引用赋值: 两者完全一样, 相当于是别名: x=[1, 2, 3], y=x 浅赋值: 第一层为复制, 内部为引用: list.copy(), y=x[:] 深复制: 全部复制, import copy; x=[1, 2]; copy.deepcopy(x)

Python 直接赋值、浅拷贝和深度拷贝解析 | 菜鸟教程 (runoob.com)

Immutable与Hashable的区别

immutable是指创建后不能修改的对象, hashable是指定义了__hash__函数的对象, 默认情况下, 用户自定义的数据类型是hashable的. 所有的immutable对象都是hashable的, 但反过来不一定.

另外还有特殊方法__eq____cmp__也与这个话题相关

类属性与实例属性

也就是说 a.name = "a" 是为实例变量赋值, python 中的类属性与 C++ 中的静态成员的处理方式是不同的:

Last updated

Was this helpful?