基本概念
编码与解析器
对于不使用 Python 默认编码的用户来说,可以在文件开头处添加
# -- coding:utf-8 --
来指明文件的编码Python3 的默认编码为是 utf-8,因此,Python3 的脚本文件无需添加该行代码
对于 Linux 用户来说,由于脚本的默认解析器为 bash,为了方便执行 Python 脚本,可以在文件开头处添加
#/usr/bin/python3
来指明脚本文件的默认解析器
鸭子类型
Python 是一种动态类型,与 C++ 等静态语言不同的是,它关注的是对象的行为而不是类型。动态语言常用的类型为 鸭子类型
鸭子类型可以概括为:一只鸟走起来像鸭子、游起泳来像鸭子、叫起来也像鸭子,那它就可以被当做鸭子
C++ 等静态语言,通过类型来构建整个语言体系,通过公有继承来维护 is-a 关系。而 Python 通过行为来构建整个语言体系,通过让对象拥有相似的行为来保证 is-a 关系。
例如对于元组来说,其可迭代,可打印,可索引(打印时元素被小括号包裹) 而对于 sqlalchemy 从数据库查到的结果来说,其可迭代,可打印,可索引(打印时元素被小括号包裹)。那么我们就认为 sqlalchemy 的结果就是元组。也可以通过类型转换转换为元组(该行为为约定)
在 Python 中,协议就是接口,而不是 interface
列表和元组
和 C++ 不一样的是,Python 中的列表和元组都是异质容器。因此它们的唯一区别就是列表是可变的,元组是不可变的
对列表解引用的语义想到与将列表在代码中展开。例如:
a = [1, 2, 3]
func(*a) # 想当于 func(1, 2, 3)
字典
Python 自 3.5 以后的字典是有序的,所谓有序是指字典索引出来的顺序和插入的顺序相同。
Python 以前字典的底层数据结构是一个哈希表,之后改成了一个“紧缩字典”。并不是和 C++ 一样的红黑树
[Python-Dev Python 3.6 dict becomes compact and gets a private version; and keywords become ordered]
类
Python 使用命名约定来决定一个类的属性。以下划线开头的函数是保护函数,以双下划线开头的是私有函数。请注意,上述只是一些命名约定,Python 使用 Name Mangling 的方式将上述形式的函数转换为对此类的调用
例如:
class A:
def _printA(self):
print("yes")
def __printA1(self):
print("no")
class B(A):
def printB(self):
self._printA()
def printB1(self):
self._A__printA1()
a = A()
a._printA()
a._A__printA1()
b = B()
b.printB()
b.printB1()
如上,以双下划线开头的函数将会视为私有函数,其函数名将被转换为 _ClassName__method_name
的形式。可以手动调用此私有函数
此外,还有以下划线开头和下划线结尾的魔法函数,这些函数一般是保留的函数,例如:
名称 | 作用 |
init | 初始化类对象 |
str | 将类对象转为字符串 |
repr | 打印对象 |
cmp | 比较大小 |
len | 返回对象长度 |
iter | 返回一个迭代器 |
new | 先于 init 调用的函数,用来返回一个类的实例 |
call | 仿函数 |
add | 加法 |
radd | 反向加法 |
del | 析构函数 |
还有一些特殊类需要实现的函数:
配合 with 语句的类需要实现:
名称 | 作用 |
enter | 进入函数时执行的任务 |
exit | 退出时执行的任务(包括因异常退出) |
模块
导入模块可以使用 import 语句,另一种方式是手动读取文件:
from importlib.machinery import SourceFileLoader
config = SourceFileLoader('config', config_file).load_module()
Python 模块搜索的顺序是:
当前路径
PYTHONPATH 下的路径
PATH 中的路径
此路径被储存到 sys.path 变量中
Python 中的每个文件就是一个模块。模块的导入只是简单的在导入位置执行一遍模块。尽管你可以选择导入哪些部分,但是实际上所有部分都会被执行,只是不需要的被舍弃了而已。例如:
#!/usr/bin/python3
print("yes")
def func():
print("func")
任何导入此模块的文件都会打印“yes”
模块可以被多次 import,但是只会被导入一次
包
在文件夹中创建 init.py
可以将当前文件夹变成一个包。此文件可以是空的,但是这样导入的时候需要指定模块
另一种方便的方式是在文件中声明需要导入的包,这样直接使用 import * from PACKAGE
就行了
装饰函数
装饰函数在以前学习的时候一直不懂什么意思,C++ 写的多了现在感觉也就一般般。只是将装饰器函数的参数是函数指针罢了。
先来一个简单的例子:
def funA(fn):
print("yes")
return fn
@funA
def funB():
print('c')
funB()
如图,装饰器 funA 传入的是一个函数指针,传出的也是一个函数指针(或者是一个可调用对象)。
装饰器的功能等价于
funB = funA(funB)
因此,如果只是 funB 的形式,只是拿了一下 funA(funB) 的返回值,需要继续调用的话使用 funB (你也可以在 funA 内自动调用 funB,这里只是为了格式上的统一)
然后就是传入参数的问题:
def funA(fn):
print("yes")
return fn
@funA
def funB(a):
print('c')
print(a)
funB(10)
没什么好说的,不明白为什么网上这么多文章要里面再嵌套一个函数,仿 js 来个闭包吗?但是 Python 中的闭包并不能修改局部变量的值
如果按照网上的来的话,形式是:
def funA(fn):
# 定义一个嵌套函数
def say(arc):
print("hello",arc)
return say
@funA
def funB(arc):
print("funB():", a)
funB("world")
唯一需要注意的就是 say 和 funB 的参数要一样,毕竟上述语句等价于:
funB = lambda fn: say
也就是说 funB 修饰后指向的实际上是 say 函数。要求参数一样自然也就没什么奇怪的了
类
私有成员
Python 类中类似 _xxx
和 __xxx
这样的函数为私有函数,否则为公有函数。 私有函数依然可以被调用,但是并不推荐
构造函数
Python 与构造函数相关的函数有两个: new
和 init
new 函数在对象创建前访问,返回类的实例
init 函数在对象创建后访问,用来初始化类成员
析构函数
在通过 del object
表达式来销毁一个对象、或者 Python 自动销毁一个对象时,将会调用类的 del
函数。
需要注意的是:Python 采用 引用记数
的方式来决定对象是否真的应该被销毁,del 操作符的作用是使引用计数减一,但是只有当计数为零时,Python 才会真的调用析构函数。
运算符重载
运算符重载分为两种:左手运算符和右手运算符
左手运算符是默认的重载运算符,意味着类对象出现在操作符左边
右手运算符是带有
r
前缀的运算符重载函数,意味着类对象出现在操作符右边
函数名 | 运算符表达式 | 解释 |
add(self,rhs) |
|
|
radd(self,rhs) |
|
|
sub(self,rhs) |
|
|
mul(self,rhs) |
|
|
truediv(self,rhs) |
|
|
floordiv(self,rhs) |
|
|
mod(self,rhs) |
|
|
pow(self,rhs) |
|
|
上述中右手运算符仅仅列出了加法。
例:
class Number:
def __add__(self, other):
print("左手加法")
def __radd__(self, other):
print("右手加法")
number = Number()
number + 1 # 调用 __add__
1 + number # 调用 __radd__
延迟计算
可以延迟计算一个属性:
import math
class Lazyproperty:
def __init__(self, func):
self.func = func
def __get__(self, instance, cls):
if instance is None:
return self
else:
value = self.func(instance)
setattr(instance, self.func.__name__, value)
return value
class Circle:
def __init__(self, radius):
self.radius = radius
@Lazyproperty
def area(self):
print('Computing area')
return math.pi * self.radius ** 2
@Lazyproperty
def perimeter(self):
print('Computing perimeter')
return 2 * math.pi * self.radius
c = Circle(1)
print(c.area)
print(c.perimeter)
另个方式是手动实现:
class OnlineIterm:
def __init__(self, info, req):
self.__info = info # __info 可能为 dict|str
self.req = req
self.info = {} # 保存类型信息
def __getattribute__(self, __name: str) -> Any:
if __name != "info":
return object.__getattribute__(self, __name)
if object.__getattribute__(self, "info") != {}:
return object.__getattribute__(self, __name)
if not hasattr(self.__info, "__call__"):
object.__setattr__(self, "info", self.__info)
return object.__getattribute__(self, __name)
object.__setattr__(self, "info", self.__info())
return self.info
这样的好处是不会丢失类型信息。
在手动实现时需要注意使用 self.attr 会调用 self.getattribute,这可能会导致无限迭代(迭代太深 python 会报错的) |
pypy
pypy 是 Python 的 JIT 实现,使用方式如下 :
pacman -S pypy3 pypy3 -m ensurepip pypy3 -m pip install requests
默认情况下,pypy 的包被安装到 ~/.local/lib/pypyxx/
中。pypy3-pip 的配置文件和 cpython 的配置文件相同
numba
numba 可以用于加速 Python 代码。numba 将一部分 Python 代码翻译成机器码从而加快速度。numba 尤其擅长处理循环:
import numba
@numba.jit(nopython=True, parallel=True)
def logistic_regression(Y, X, w, iterations):
for i in range(iterations):
w -= np.dot(((1.0 /
(1.0 + np.exp(-Y * np.dot(X, w)))
- 1.0) * Y), X)
return w
生成器
如果函数中存在 yield,那么函数的返回值就是生成器类型。生成器迭代器的一个子类。但是提供了比迭代器更多的功能。一个生成器的类型注释为 :
Generator[YieldType, SendType, ReturnType]
迭代器语法为 :
return_val yield value
其中,return_val 是 SendType,value 是 YieldType。而如果函数后面存在 return,那么 return 后面的值是 ReturnType
|
一个代码示例为:
from typing import Generator
import logging
logging.basicConfig(level=logging.DEBUG, format="%(lineno)s: %(message)s")
def test_generator() -> Generator[int, float, str]:
for i in range(5):
b = yield i
logging.debug(b)
return "a"
a = test_generator()
logging.debug(next(a)) # i = 0
logging.debug(a.send(10)) # i = 1
logging.debug(a.send(20)) # i = 2
logging.debug(a.send(30)) # i = 3
logging.debug(a.send(40)) # i = 4
logging.debug(next(a)) # Exception: StopIteration: a
可以看到,next 和 send 都会将函数执行到下一个 yield 的位置,并将 YieldType 返回。但是 send 可以向函数发送一个值
可以将 send 简单地用作推进生成器进展,其作用与 next 相同。即使函数不使用 yield 的返回值也一样。 |
对生成器解引用会得到一个 展开 的列表
例如:
from typing import Generator
def test_generator() -> Generator[int, None, None]:
for i in range(10):
yield i
a = test_generator()
print(*a) # 这里相当于 print(0,1,2,3,4,5,6,7,8)