Python Tips for Impatient Dev

22-01-31 编程 #code #python

Python tricks

f-string 的妙用

py3.6 开始,推荐使用 f-string,不要使用 %s或者 "".format().如果接收用户输入,使用 Template 做安全校验。
在 python f-string 中可以通过变量或者表达式后面加=实现打印变量名或者表达式:

print(f'{v=}') # 等价 print(f'v={v}')
print(f'{(len(arr),v)=}') 

参考:调式时icecreamprint log更好。

python 注解

Python 对注解所做的唯一的事情,就是将它们存储在函数的 annotations 属性中

>>> def foo(a: 'x', b: 5 + 6, c: list) -> max(2, 9): ...
>>> foo.__annotations__
{'a': 'x', 'b': 11, 'c': <class 'list'>, 'return': 9}

单例模式

参考creating-a-singleton-in-python
,建议是 metaclass 模式,里面很多回答是错误的。

# 下面其实不是单例,因为可以再次调用 Foo(), 只是一个全局变量
class Foo(object):
     pass

some_global_variable = Foo()
# 利用缓存可以实现单例模式,前提是__init__没有参数,因为 cache 是根据参数列表生成缓存的 key
from functools import cache

@cache
class CustomClass(object):

    def __init__(self): #这里不能有参数,因为会导致 key 不一样
        self.instance =  ... 


# 另外一种方法是 metaclass, 和上面的@cache 原理近似,只不过一个是外部缓存,一个是 mataclass 内缓存,同理,__init__不能有参数
class Singleton(type):
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]
        
class ESClient(metaclass=Singleton):
    pass

assert id(ESClient()) == id(ESClient())


# 一种取巧的方式
def _singleton(c): c()

@_singleton
class ESClient(object):

    def __init__(self):
        self.client =  Elasticsearch([{'host': es_config.host, 'port': es_config.port}])
    def get_instance(self):
        """get es client instance"""
        return self.client
# 原理是通过装饰器改写后得到的 wat 实际是一个对象,不是一个 class:
wat = _singleton(wat) # 这样就阻止你通过 another_instance = wat() 获取新实例
# 当然你可以通过 another_instance = wat.__class__() 创建新的实例
# 当然这种方式也要求__init__不能有其他参数,对于无参的单例模式其实挺巧妙

# 补充阅读 python es client 是否应该全局变量:
# https://elasticsearch-py.readthedocs.io/en/7.x/#thread-safety

枚举类 Enum 略去 value 方法

假设你想要获得下面Color#000, 需要使用Color.WHITE.value。但是可以通过StrEnum省去这个.value

class Color(Enum):
    WHITE = "#000"

# tips
from enum import StrEnum

class Directions(StrEnum):
    NORTH = 'north',    
    SOUTH = 'south',     # notice the trailing comma, it's ok and recommend

print(Directions.NORTH) # no need .value
# 缺点:StrEnum 成员不能有 int 类型

使用 with 管理需要关闭的资源,无需多个 with

with open(file_path, 'w') as file, get_connection(config) as conn:
    file.write('Hello, World!')

简化 if 的一些技巧

if variable == a or variable == b:
    res = do_something(variable)

# better  
if variable in {a, b}:
    res = do_something(variable)

if 'a' in v or 'b' in v or 'c' in v:
  pass

# better
if any(char in v for char in ('a', 'b', 'c')):
    pass


if b > 10:
    a = 0
else:
    a = 5
# better 
a = 0 if b > 10 else 5

if data:
    lst = data
else:
    lst = [0, 0, 0]
    
# better, only use this when data type is collection, 
# cuz if data is int, then data=0 will make lst = [0,0,0]
lst = data or [0, 0, 0]

Instead of asking for permission, ask for forgiveness

Python 的异常比较轻量,所以一般推荐的写法是与其提前判断条件是否满足,不如在 try catch 中处理异常

try:
    with open(filename, 'w') as file:
        file.write('Hello, World!')

except FileNotFoundError:
    print('File does not exist.')

except PermissionError:
    print('You dont have write permission.')

except OSError as exc:
    print(f'An OSError has occurred:\n{exc}')

isinstance

isinstance 可以一次判断多个 Class 类型:

# no need or conditions
isinstance(foo, (Class1, Class2, ...))

海象运算符 (walrus operator)

python 3.8 引入海象运算符,解决一个场景:获取一个值,检查它是否为非零,然后使用它。在 python3.8 之前需要三行:

count = fresh_fruit.get('lemon', 0)
if count:
    make_lemonade(count)
else:
    out_of_stock()

上面的代码会引入一个看上去非常重要的变量count,但实际情况并非如此,count 只在一个分支里面使用到。使用海象运算符可以解决这个问题。

if count := fresh_fruit.get('lemon', 0):
    make_lemonade(count)
else:
    out_of_stock()

虽然只是减少了一行代码,但是可读性提升了巨大,可以清楚地知道 count 只有在 if 成立时才使用到。甚至还可以在 if 中判断

if (count := fresh_fruit.get('apple', 0)) >= 4:
    make_cider(count)
else:
    out_of_stock()

另一个常见的场景是 do-while,例如处理一个分页请求,直到请求返回为 None,由于 python 不支持 do-while 语法,一般有两种写法:

page_data = get_page()
while page_data:
    process_page(page_data)
    page_data = get_page()

# 方法 2 loop-and-a-half
while True:
    page_data = get_page()
    if not page_data:
        break
    process_page(page_data)

海象运算符完美解决了这个问题:

while page_data := get_page():
    process_page(page_data)

读写文件时也经常遇到这个场景:

fp = open("test.txt", "r")
while line := fp.readline():
    print(line.strip())

多进程的使用

from multiprocessing import Pool

requests = [req1, req2]

with Pool as p:
    results = p.map(process_request, requests)

python 的 dict 中关于 equal 和 hash 计算方式会有意外的效果

['no', 'yes'][True] # output?
{True: 'yes', 1: 'no', 1.0: 'maybe'} # output?

“布尔类型是整数类型的子类型,布尔值在几乎所有环境中的行为都类似于值 0 和 1,但在转换为字符串时,分别得到的是字符串 False
或 True.”
– The Standard Type Hierarchy

由于 True,1, 1.0 的__eq__和__hash__都一样,所以出现了神奇的结果。

(1) != (1,) 第一个是 int,第二个是 tuple

避免可变的默认参数,例如:

def fun(count=[]):
  count.append(2) #这里 count 两次调用如果都使用默认参数的话,则是同一个数组,非常危险!
  return count
fun()   #[2]
fun()   #[2,2]

同理,需要避免在 tuple 中放入可变类型元素,例如 list。

flat seq: str, bytes, bytesarray, memeoryview, array.array
container seq: list, tuple, collections.deque

immutable: bytes, str, tuple
mutable: list dict set bytesarray

staticmethod classmethod

staticmethod 和 classmethod 都可以通过 Cls.m() 或 instance.m() 方式访问,都可以被继承,都可以访问全局变量。区别是
classmethod 访问的 class 变量信息会自动在 Derive 子类中改变,而 staticmethod 因为缺少第一个 cls 参数,所以访问的全局变量始终是父类的变量。
staticmethod 可以理解为 Java 的 StringUtils 类,只是和 Cls 放在一起方便代码阅读和组织。
classmethod 则是可以通过 cls 参数访问到当前类信息的。

容器方法

合并字典

d1.update(d2) # 遍历 d2,更新到 d1
d = dict(**profile, **ext_info) #解构重新创建 dict,右边的优先级高
d = dict(profile.items() | ext_info.items()) #同理
d = d1 | d2 # 新语法
{k: v for d in [profile, ext_info] for k, v in d.items()} # 推导式

py3.6 开始 dict 默认插入有序,如果只是希望插入有序则无需使用 OrderDict

dict get and pop

d.get(k,default) 
d.setdefault(k,default) 
d.pop(k) #删除不存在的键时,使用 del d[k] 有异常抛出,pop 则不会

自定义 dict

自定义自己的 dict 不能继承 dict,而是 collections.abc.MutableMapping, 因为自带的 list 和 dict 有一些特殊行为无法覆盖

sort dict by key:

print(sorted(dic, key=dic.get)) #output: key in asc order

带索引遍历

for idx, item in enumerate(x):

浅复制

list(l) 等方式构建的 list dict set 属于浅复制,如果容器的元素还是容器,那么元素属于引用。
深度复制需要使用 copy module

import copy
xs = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
zs = copy.deepcopy(xs)
# 对象同样可以使用 copy,还有__copy__等魔法方法可以探索

使用 namedtuple

namedtuple 可以让代码更容易阅读,当然现在可以是所有 dataclass,特性更丰富

from collections import namedtuple
class Car:
  """you do not modify the name and date attributes """

  def __init__(self, name, date):
      self._name = name
      self._date = date


# use namedtuple
Car = namedtuple('Car', 'name date')  # 注意,这里多个属性可以一次性传入,使用空格分割
# 然后你可以将 Car 作为 data class 使用

#还有一个 type hint 的版本
from typing import NamedTuple

list vs array

list 的元素可以不同,更为紧凑的单一类型是 array 类型。
list 通过 append pop 两个方法可以实现 stack 效果。

Counter

Counter(string).most_common(3)

deque

from collections import deque
names = deque(['raymond', 'rachel', 'matthew', 'roger',
             'betty', 'melissa', 'judith', 'charlie'])
names.popleft()
names.appendleft('mark')

一些不常用的容器

CountedObject,ChainMap,MappingProxyType,frozenset,defaultdict

在遍历中修改 list

使用 for i in range(len(a)) 或者 for i, v in enumerate(a) 都是危险的。

# 方法 1 将 list 拷贝一下,遍历新数组的过程中,修改原 list:
num_list = [1, 2, 3, 4, 5]
print(num_list)
for item in num_list[:]:  # 这里 list[:] 是对原数组的拷贝,trick!!!
    if item == 2: 
        num_list.remove(item)
    else:
        print(item)
print(num_list)

# 方法 2 如果数组很大,那应该使用倒序遍历:
for i in range(len(num_list)-1, -1, -1):
  if num_list[i] == 2:
      num_list.pop(i)
  else:
      print(num_list[i])
      
# 方法 3 还是使用 for 推导式
[4 if x==3 else x for x in num_list]

打印容器可以使用 pprint.pprint

str 和 bytes

bytes 和 bytesarray

bytes 是不可变的数组,每个元素必须在 0~255 之间。
bytearray 是可变的,可以修改,增加,删除元素。
转换 bytes(ba)

多行 string

my_very_big_string = (
  "For a long time I used to go to bed early. Sometimes, "
  "when I had put out my candle, my eyes would close so quickly "
  "that I had not even time to say “I’m going to sleep.”"
)

多行文本去除缩进可以使用 textwrap.deden("""\your text""")

str 重复 n 次

print(s * n);

int/string intern

小整数池是[-5,256], string 也有 string intern

str.partition/str.translate

有时使用 str.partition 方法拆分 str 或者 str.translate 批量 replace 子串更方便

NamedTuple, typing.NamedTuple, dataclass

from collections import namedtuple
Coordinate = namedtuple('Coordinate', 'lat long')
issubclass(Coordinate, tuple) # True
moscow = Coordinate(55.756, 37.617)
moscow == Coordinate(lat=55.756, long=37.617)  # True __eq__

# typing.NamedTyple also is subclass tuple, not new type 
import typing
Coordinate = typing.NamedTuple('Coordinate', [('lat', float), ('long', float)])
# or with type: Coordinate = typing.NamedTuple('Coordinate', lat=float, long=float)
issubclass(Coordinate, tuple) # True
Coordinate.__annotations__
# {'lat': <class 'float'>, 'long': <class 'float'>}”
issubclass(Coordinate, typing.NamedTuple)
# False
issubclass(Coordinate, tuple)
# True

### class style
from typing import NamedTuple

class Coordinate(NamedTuple):

    lat: float
    long: float

    def __str__(self):
        ns = 'N' if self.lat >= 0 else 'S'
        we = 'E' if self.long >= 0 else 'W'
        return f'{abs(self.lat):.1f}°{ns}, {abs(self.long):.1f}°{we}'

### dataclass
from dataclasses import dataclass

@dataclass(frozen=True) # frozen=True: exception if re-assign fields after instance initialized
class Coordinate:

    lat: float
    long: float

    def __str__(self):
        ...

dataclasses.asdict(c)

重要标准库

functools contextlib atexit pathlib collections itertools
inspect: e.g. inspect the function signature
bisect: 二分查找

pathlib vs os.path

use pathlib over os.path. 后者方法不全。

def project_root() -> Path:
    return Path(__file__).parent.parent.parent


def file_abspath(relative_path: str) -> str:
    """get file absolute path from project root"""
    return (project_root() / relative_path).as_posix()

os vs sys

os module is for system, sys this module provides access to some variables used or maintained by
the interpreter and to functions that interact strongly with the interpreter.

迭代器

pythonic 的代码

其他

参考资料

Python 工匠系列
RealPython 学习路径
一份非常详尽的 Python 小抄
Fluent Python 2nd