Python進階之道

NO IMAGE

Python 和 JavaScript 在筆者看來是很相似的語言,本文歸納了 Python 的語言專屬特性。

語言專屬特性

推導式

推導式是一種快速構建可迭代對象的方法,因此凡是可迭代的對象都支持推導式

列表推導式

獲取 0-10 內的所有偶數

even = [i for i in range(10) if not i % 2]
even
# [0, 2, 4, 6, 8]

字典推導式

將裝滿元組的列表轉換為字典

SEIREI = [(0, 'takamiya mio'), (1, 'tobiichi origami'), (2, 'honjou nia'), (3, 'tokisaki kurumi'), (4, 'yoshino'), (5, 'itsuka kotori'), (6, 'hoshimiya mukuro'), (7, 'natsumi'), (8, 'yamai'), (9, 'izayoi miku'), (10, 'yatogami tohka')]
seirei_code = {seirei: code for code, seirei in SEIREI}
seirei_code
# {'takamiya mio': 0, 'tobiichi origami': 1, 'honjou nia': 2, 'tokisaki kurumi': 3, 'yoshino': 4, 'itsuka kotori': 5, 'hoshimiya mukuro': 6, 'natsumi': 7, 'yamai': 8, 'izayoi miku': 9, 'yatogami tohka': 10}
{code: seirei.upper() for seirei, code in seirei_code.items() if code > 6}
# {7: 'NATSUMI', 8: 'YAMAI', 9: 'IZAYOI MIKU', 10: 'YATOGAMI TOUKA'}

集合推導式

求所有數字的平方並去除重複元素

{x ** 2 for x in [1, 2, 2, 3, 3]}
# {1, 4, 9}

生成器表達式

求 0-10 內的所有偶數的和

even_sum_under_10 = sum(i for i in range(11) if not i % 2)
even_sum_under_10
# 30

解包

最典型的例子就是 2 數交換

a, b = b, a
# 等價於 a, b = (b, a)

用星號運算符解包可以獲取剩餘的元素

first, *rest = [1, 2, 3, 4]
first
# 1
rest
# [2, 3, 4]

用下劃線可以忽略某個變量

filename, _ = 'eroge.exe'.split('.')
filename
# 'eroge'

星號運算符

數據容器的合併

l1 = ['kaguya', 'miyuki']
l2 = ['chika', 'ishigami']
[*l1, *l2]
# ['kaguya', 'miyuki', 'chika', 'ishigami']
d1 = {'name': 'rimuru'}
d2 = {'kind': 'slime'}
{**d1, **d2}
# {'name': 'rimuru', 'kind': 'slime'}

函數參數的打包與解包

# 打包
def foo(*args):
print(args)
foo(1, 2)
# (1, 2)
def bar(**kwargs):
print(kwargs)
bar(name='hayasaka', job='maid')
# {'name': 'hayasaka', 'job': 'maid'}
# 解包
t = (10, 3)
quotient, remainder = divmod(*t)
quotient
# 商:3
remainder
# 餘:1

裝飾器

裝飾器是一個可調用的對象,顧名思義它能夠裝飾在某個可調用的對象上,給它增加額外的功能

常用於緩存、權限校驗、日誌記錄、性能測試、事務處理等場景

以下實現了一個簡單的日誌裝飾器,能打印出函數的執行時間、函數名、函數參數和執行結果

import time
from functools import wraps
def clock(func):
@wraps(func) # 防止被裝飾函數的屬性被wrapper覆蓋
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs) # 由於閉包,wrapper函數包含了自由變量func
elapsed = time.perf_counter() - t0
name = func.__name__
args = ', '.join(repr(arg) for arg in args)
kwargs = ', '.join(f'{k}={w}' for k, w in sorted(kwargs.items()))
all_args_str = ', '.join(astr for astr in [args_str, kwargs_str] if astr)
print(f'[{elapsed:.8f}s] {name}({all_args_str}) -> {result}')
return result
return wrapper # 返回內部函數,取代被裝飾的函數
@clock
def factorial(n: int) -> int:
return 1 if n < 2 else n * factorial(n-1)
factorial(5)
# [0.00000044s] factorial(1) -> 1
# [0.00011111s] factorial(2) -> 2
# [0.00022622s] factorial(3) -> 6
# [0.00030844s] factorial(4) -> 24
# [0.00042222s] factorial(5) -> 120
# 120

如果想讓裝飾器能接受參數,那就要再嵌套一層

import time
from functools import wraps
DEFAULT_FMT = '[{elapsed:.8f}s] {name}({all_args_str}) -> {result}'
def clock(fmt=DEFAULT_FMT):
def decorate(func):
@wraps(func)
def wrapper(*args, **kwargs):
t0 = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - t0
name = func.__name__
args_str = ', '.join(repr(arg) for arg in args)
kwargs_str = ', '.join(f'{k}={w}' for k, w in sorted(kwargs.items()))
all_args_str = ', '.join(astr for astr in [args_str, kwargs_str] if astr)
print(fmt.format(**locals()))
return result
return wrapper
return decorate
@clock()
def factorial_default_fmt(n: int) -> int:
return 1 if n < 2 else n * factorial_default_fmt(n-1)
@clock('{name}: {elapsed}s')
def factorial_customed_fmt(n: int) -> int:
return 1 if n < 2 else n * factorial_customed_fmt(n-1)
factorial_default_fmt(3)
# [0.00000044s] factorial_default_fmt(1) -> 1
# [0.00009600s] factorial_default_fmt(2) -> 2
# [0.00018133s] factorial_default_fmt(3) -> 6
# 6
factorial_customed_fmt(3)
# factorial_customed_fmt: 4.444450496521313e-07s
# factorial_customed_fmt: 9.733346314533264e-05s
# factorial_customed_fmt: 0.0001831113553407704s
# 6

在 django 中,可以通過裝飾器對函數視圖進行功能增強(比如@login_required 進行登錄的權限校驗,@cache_page 進行視圖的緩存等)

上下文管理器

用於資源的獲取與釋放,以代替 try-except 語句

常用於文件 IO,鎖的獲取與釋放,數據庫的連接與斷開等

# try:
#     f = open(input_path)
#     data = f.read()
# finally:
#     f.close()
with open(input_path) as f:
data = f.read()

其實在 pathlib 裡已經給我們封裝好了文件 IO 方法

# with open('file') as i:
#     data = i.read()
from pathlib import Path
data = Path('file').read_text()

至於上下文管理器的實現,可以用@contextmanager

from contextlib import contextmanager
@contextmanager
def open_write(filename):
try:
f = open(filename, 'w')
yield f
finally:
f.close()
with open_write('onegai.txt') as f:
f.write('Dagakotowaru!')

多重繼承

在 django 中經常要處理類的多重繼承的問題,這時就要用到 super 函數

如果單單認為 super 僅僅是“調用父類的方法”,那就錯了

在繼承單個類的情況下,可以認為 super 是調用父類的方法(ES6 裡面亦是如此)

但多重繼承就不一樣了,因為方法名可能會有衝突,所以 super 就不能單指父類了

在 Python 中,super 指的是 MRO 中的下一個類,用來解決多重繼承時父類的查找問題

MRO 是啥?Method Resolution Order(方法解析順序)

看完下面的例子,就會理解了

class A:
def __init__(self):
print('A')
class B(A):
def __init__(self):
print('enter B')
super().__init__()
print('leave B')
class C(A):
def __init__(self):
print('enter C')
super().__init__()
print('leave C')
class D(B, C):
pass
d = D()
# enter B
# enter C
# A
# leave C
# leave B
print(d.__class__.__mro__)
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

首先,因為 D 繼承了 B 類,所以調用 B 類的__init__,打印了enter B

打印enter B後的 super 尋找 MRO 中的 B 的下一個類,也就是 C 類,並調用其__init__,打印enter C

打印enter C後的 super 尋找 MRO 中的 C 的下一個類,也就是 A 類,並調用其__init__,打印A

打印A後回到 C 的__init__,打印leave C

打印leave C後回到 B 的__init__,打印leave B

特殊方法

在 django 中,定義 model 的時候,希望 admin 能顯示 model 的某個字段而不是 XXX Object,那麼就要定義好__str__

每當你使用一些內置函數時,都是在調用一些特殊方法,例如 len()調用了__len__(), str()調用__str__()等

以下實現一個 2d 數學向量類,裡面有多個特殊方法

from math import hypot
class Vector2d:
# 限制允許綁定的屬性
__slots__ = ('__x', '__y')
# 實例創建
def __init__(self, x, y):
self.__x = float(x)
self.__y = float(y)
# 前雙下劃線是私有屬性,property裝飾是隻讀屬性
@property
def x(self):
return self.__x
@property
def y(self):
return self.__y
# 可迭代對象
def __iter__(self):
yield from (self.x, self.y)
# 字符串表示形式
def __repr__(self) -> str:
return f'{type(self).__name__}({self.x}, {self.y})'
# 數值轉換 - 絕對值
def __abs__(self) -> float:
return hypot(self.x, self.y)
# 數值轉換 - 布爾值
def __bool__(self) -> bool:
return bool(abs(self))
# 算術運算符 - 加
def __add__(self, other):
x = self.x + other.x
y = self.y + other.y
return Vector2d(x, y)
# 算術運算符 - 乘
def __mul__(self, scalar: float):
return Vector2d(self.x * scalar, self.y * scalar)
# 比較運算符 - 相等
def __eq__(self, other):
return tuple(self) == tuple(other)
# 可散列
def __hash__(self):
return hash(self.x) ^ hash(self.y)
v = Vector2d(3, 4)
# __slots__限制了允許綁定的屬性,只能是x或y
v.z = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: 'Vector2d' object has no attribute 'z'
# 由於x屬性只讀,因此無法再次賦值
v.x = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# AttributeError: can't set attribute
# iter(v) => v.__iter__()
x, y = v
# x為3, y為4
# repr(v) => v.__repr__()
v
# Vector2d(3, 4)
# abs(v) => v.__abs__()
abs(v)
# 5.0
# bool(v) => v.__bool__()
bool(v)
# True
# v1 + v2  => v1.__add__(v2)
v1 = Vector2d(1, 2)
v2 = Vector2d(3, 4)
v1 + v2
# Vector2d(4, 6)
# v * 3  => v.__mul__(3)
v * 3
# Vector2d(9, 12)
# v1 == v2 => v1.__eq__(v2)
v1 = Vector2d(1, 2)
v2 = Vector2d(1, 2)
v1 == v2
# True
# hash(v) => v.__hash__()
hash(v)
# 7
v1 = Vector2d(1, 2)
v2 = Vector2d(3, 4)
set([v1, v2])
# {Vector2d(1.0, 2.0), Vector2d(3.0, 4.0)}

如果把 Vector 改造為多維向量呢?關鍵就是要實現序列協議(__len__和__getitem__)

協議:本質上是鴨子類型語言使用的非正式接口

不僅如此,還要實現多分量的獲取以及散列化

from liay import liay
import reprlib
import math
import numbers
import string
from functools import reduce
from operator import xor
from itertools import zip_longest
import numbers
from fractions import Fraction as F
class Vector:
typecode = 'd'
shortcut_names = 'xyzt'
def __init__(self, components):
self._components = liay(self.typecode, components)
def __iter__(self):
return iter(self._components)
def __repr__(self):
components = reprlib.repr(self._components)
components = components[components.find('['):-1]
return f'{type(self).__name__}({components})'
def __str__(self):
return str(tuple(self))
def __eq__(self, other):
return tuple(self) == tuple(other)
def __bool__(self):
return bool(abs(self))
# 序列協議 - 獲取長度
def __len__(self):
return len(self._components)
# 序列協議 - 索引取值
def __getitem__(self, index):
cls = type(self)  # Vector
if isinstance(index, slice):  # 索引是slice對象,則返回Vector實例
return cls(self._components[index])
elif isinstance(index, numbers.Integral):  # 索引是整數類型,則返回_components中對應的數字
return self._components[index]
else:
raise TypeError(f'{cls.__name__} indices must be integers.')
# 屬性訪問,獲取分量的值
def __getattr__(self, name):
cls = type(self)
if len(name) == 1:
pos = cls.shortcut_names.find(name)
if 0 <= pos < len(self._components):
return self._components[pos]
raise AttributeError(f'{cls.__name__} has no attribute {name}')
# 屬性設置,給分量設值時會拋出異常,使向量是不可變的
def __setattr__(self, name, value):
cls = type(self)
if len(name) == 1:
if name in string.ascii_lowercase:
raise AttributeError(f"can't set attribute 'a' to 'z' in {cls.__name__}")
super().__setattr__(name, value)
# 比較所有分量,都相等才算兩向量相等
def __eq__(self, other):
return len(self) == len(other) and all(a == b for a, b in zip(self, other))
# 散列化
def __hash__(self):
hashes = map(hash, self._components)
return reduce(xor, hashes, 0)
# 絕對值
def __abs__(self):
return math.sqrt(sum(x ** 2 for x in self))
# 取正
def __pos__(self):
return Vector(self)
# 取負
def __neg__(self):
return Vector(-x for x in self)
# 加 (減法__sub__的實現與之類似,略)
def __add__(self, other):
try:
return Vector(a + b for a, b in zip_longest(self, other, fillvalue=0.0))
except TypeError:
return NotImplemented
# 反向加(a+b中,如果a沒有__add__或返回NotImplemented,則檢查b是否有__radd__,有則調用之)
def __radd__(self, other):
return self + other
# 乘 (除法__truediv__的實現與之類似,略)
def __mul__(self, scalar):
return Vector(n * scalar for n in self) if isinstance(scalar, numbers.Real) else NotImplemented
# 反向乘
def __rmul__(self, scalar):
return self * scalar
# 中綴運算符@ - 點積
def __matmul__(self, other):
try:
return sum(a * b for a, b in zip(self, other))
except TypeError:
return NotImplemented
# 反向中綴運算符@
def __rmatmul__(self, other):
return self @ other
v = Vector(range(7))
v
# Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
v[1:3]
# Vector([1.0, 2.0])
v[-1]
# 6.0
v[1,3]
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 39, in __getitem__
# TypeError: Vector indices must be integers.
v.x, v.y, v.z
# (0.0, 1.0, 2.0)
v.x = 1
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 62, in __setattr__
# AttributeError: can't set attribute 'a' to 'z' in Vector
v1 = Vector((3, 4, 5))
v2 = Vector((6, 7))
v1 == v2
# False
set([v1, v2])
# {Vector([6.0, 7.0]), Vector([3.0, 4.0, 5.0])}
abs(v)
# 9.539392014169456
+v
# Vector([0.0, 1.0, 2.0, 3.0, 4.0, ...])
-v
# Vector([-0.0, -1.0, -2.0, -3.0, -4.0, ...])
v1 + v2
# Vector([9.0, 11.0, 5.0])
v * 3
# Vector([0.0, 3.0, 6.0, 9.0, 12.0, ...])
v * F(1, 2)
# Vector([0.0, 0.5, 1.0, 1.5, 2.0, ...])
v1 @ v2
# 46.0

想了解所有的特殊方法可查閱官方文檔,以下列舉些常用的:

字符串表示形式:__str__, __repr__
數值轉換:__abs__, __bool__, __int__, __float__, __hash__
集合模擬:__len__, __getitem__, __setitem__, __delitem__, __contains__
迭代枚舉:__iter__, __reversed__, __next__
可調用模擬:__call__
實例創建與銷燬:__init__, __del__
屬性訪問:__getattr__, __setattr__
運算符相關:__add__, __radd__, __mul__, __rmul__, __matmul__, __rmatmul__, ...

類方法和靜態方法

@classmethod 是類方法,它定義操作類的方法,也就是說會將類綁定給方法,而不是實例

@staticmethod 是靜態方法,啥都不綁定,一般用來給類綁定各種工具方法(不涉及對實例和類的操作)

在 django 中,我們經常要在視圖函數中對模型類進行各種查詢

然而,很多查詢都是重複的代碼,根據 DRY 原則,它們都是可以被封裝的

那麼,如果我們要給模型類封裝一些查詢操作,就要用到@classmethod

以下是 Post 類,裡面定義了 latest_posts 方法用來獲取最新的幾個 Post

這樣在視圖函數中,就能直接調用該方法進行查詢,節省了不少代碼

class Post(models.Model):
STATUS_NORMAL = 1
STATUS_DELETE = 0
STATUS_DRAFT = 2
STATUS_ITEMS = (
(STATUS_NORMAL, '正常'),
(STATUS_DELETE, '刪除'),
(STATUS_DRAFT, '草稿'),
)
...
status = models.PositiveIntegerField(_("狀態"), choices=STATUS_ITEMS, default=STATUS_NORMAL)
created_time = models.DateTimeField(_("創建時間"), auto_now_add=True)
...
@classmethod
def latest_posts(cls, limit=None):
queryset = cls.objects.filter(status=cls.STATUS_NORMAL).order_by('-created_time')
if limit:
queryset = queryset[:limit]
return queryset

描述符

實現了__set__或__get__協議的類就是描述符

set 和 get 代表存和取,因此描述符是一種對多個類屬性運用相同存取邏輯的一種方式

例如 django 的 ORM 中的字段類型是描述符,用來把數據庫記錄中的字段數據與 Python 對象的屬性對應起來

以下實現一個簡單的描述符類,用來在讀寫屬性時驗證屬性的正確性

class Validator:
def __init__(self, storage_name):
self.storage_name = storage_name
def __set__(self, instance, value):
if not isinstance(value, int):
raise ValueError('Value must be an integer')
if value > 200:
raise ValueError('Value must be under 200')
class Person:
age = Validator('age')
def __init__(self, age):
self.age = age
person = Person(age=100)
person.age = 'young'
# Traceback (most recent call last):
# ValueError: Value must be an integer
person.age = 201
# Traceback (most recent call last):
# ValueError: Value must be under 200

元類

進入元類這個概念之前,我們先回顧一下 type()這個函數,不,其實它是個類

通過 type(),我們可以獲取一個對象所屬的類,但通過 help 函數,發現 type()居然也可以用來創建類!

type(name, bases, dict) -> a new type

name 是新類的名稱,bases 是繼承的子類,dict 則是新類的屬性名與其對應值的字典

class A:
a = 1
def foo(self):
return self.a * 2
# 以上類的創建等價於
A = type('A', (object, ), {'a': 1, 'foo': lambda self: self.a * 2})

那麼什麼是元類呢?

平時我們用類來創建對象,但一切類都繼承了對象,說白了類也是對象,而元類就是用來創建類對象的類

說白了,元類就是製造類的工廠

'alphardex'.__class__
# <class 'str'>
'alphardex'.__class__.__class__
# <class 'type'>

通過以上的例子我們知道 type 就是用來創造一切類的元類,它是 Python 內置的元類

既然有內置的元類,也意味著你也可以自定義元類

以下實現一個元類,用來把類的所有非私有屬性自動轉換為大寫(不已_開頭的屬性都是非私有的)

思路很簡單:把屬性和對應的值字典(attrs)裡的非私有屬性鍵改為大寫(upper)就行了

class UpperAttrMeta(type):
def __new__(cls, name, bases, attrs):
"""
__init__方法用來初始化對象並傳入參數
而__new__方法專門用來創建對象(顯然這裡我們要創建一個類對象並定製它)
"""
upper_attrs = {k.upper() if not k.startswith('_') else k: v for k, v in attrs.items()}
return super().__new__(cls, name, bases, upper_attrs)
class Foo(metaclass=UpperAttrMeta):
name = 'alphardex'
__love = 'unknown'
f = Foo()
f.NAME
# 'alphardex'
f._Foo__love
# 'unknown'

元類的最經典的用途就是 ORM 的實現,以 django 的 ORM 為例

class Person(models.Model):
name = models.CharField(max_length=30)
age = models.IntegerField()
p = Person(name='alphardex', age='24')
p.age
# 24

如果你訪問一個模型實例的屬性(例如這裡的 age),你並不會得到什麼 IntegerField(),而是得到了 24 這個數字,這就是元類的作用

元類平時很少用到,如果要動態修改類的屬性,可以用猴子補丁(直接修改類方法)或者類裝飾器

當然,這並不代表元類沒什麼用,想用到它的時候自然會用到的

相關文章

DOM的常用API速查表

一個孤獨中二患者的自白|年度徵文

前端必備的實用工具,都在這裡了

JavaScript進階之道