Python语法指南

python易学易用,但也有很多容易错用的特性,或者有一些高级特性可以大大提升开发的性能或效率,这里记录一下。

赋值、浅拷贝和深拷贝(Shallow and deep copy)

赋值只是赋予一个对象新的名字,相当于原对象的引用,类似于C/C++的指针,他们的ID(所指向的内存)都是相同的 ,因此对任何一个操作,对另一个也会生效。

浅拷贝 copy.copy() 会生成一个新的对象,ID和原对象不同,但这个对象并非是原对象的引用,而是原对象内第一层元素对象的引用。

Deep Copy相当于完全复制了一个全新的对象,对对象中所有元素以递归的形式复制到新的对象中,因此新、旧对象完全独立,彼此的操作互不影响:

% python
>>> import copy
>>> a = [[1,2],['a','b']]
>>> a2 = a
>>> a3=copy.copy(a)
>>> a4=copy.deepcopy(a)
>>> a.append('xxx')
>>> a[0].append(3)
>>> a[1] = ['c','d']
>>> a
[[1, 2, 3], ['c', 'd'], 'xxx']
>>> a2
[[1, 2, 3], ['c', 'd'], 'xxx']
>>> a3
[[1, 2, 3], ['a', 'b']]
>>> a4
[[1, 2], ['a', 'b']]

需要注意的是,Python的变量分为可变(mutable)变量和不可变(immutable)变量,对于不可变变量(数字、字符串、元组)来说,赋值和Shallow copy都类似深拷贝,即传值而不是引用。可变变量(dict、list、set、object)才有深浅拷贝的区分。

Class Variable and Class Method

类变量和类方法类似于C++中的静态方法,和对象的变量和方法是不同的,对类变量的修改需要使用类名做前缀,任何对象作为前缀的修改都是修改对象的变量而不是类变量:

class A:
    a = 1
    def __init__(self):
        self.a = 2

    @classmethod
    def f(cls):
        print(cls.a, end=' ')

    def g(self):
        print(self.a, end=' ')

if __name__ == '__main__':
    a1 = A()
    a2 = A()
    A.f()
    a1.f()
    a2.f()
    a1.g()
    a2.g()
    A.a = 3
    a1.a = 4
    A.f()
    a1.f()
    a2.f()
    a1.g()
    a2.g()
    print(a1.a, a2.a)

# 执行结果
#   1 1 1 2 2 3 3 3 4 2 4 2

coroutine 和async await

从3.5版本开始支持新的语法(PEP 492 ),从3.7开始async和await成为保留关键字,3.6、3.7、3.8、3.9还在不断的完善这个功能之中。

import time
import requests
import asyncio

async def test2(i):
  r = await other_test(i)
  print(f'{i} - Done, {r.status_code}')
 
async def other_test(i):
  r = requests.get(i)   
  await asyncio.sleep(4)
  print(f'{i} -  {(time.time() - start):.3f}')
  return r
 
url = ["https://www.baidu.com/s?wd=python",
        "https://www.baidu.com/s?wd=asyncio",
        "https://www.baidu.com/s?wd=await"]
 
loop = asyncio.get_event_loop()
task = [asyncio.ensure_future(test2(i)) for i in url]
start = time.time()
loop.run_until_complete(asyncio.wait(task))
endtime = time.time() - start
print(f"All used: {endtime:.3f}")
loop.close()

除了async def之外,async还支持for和with语句,但只能在协程函数中使用,用于实现对异步iter的遍历:

import time
import requests
import asyncio

url = ["https://www.baidu.com/s?wd=python",
        "https://www.baidu.com/s?wd=asyncio",
        "https://www.baidu.com/s?wd=await"]

async def get_url():
  for i in url:
    await asyncio.sleep(3)
    yield requests.get(i)

async def get_res():
  start = time.time()
  async for r in get_url():
    print(f'{r.url} -  {(time.time() - start):.3f}') 
  else:
    print('Done')
asyncio.run(get_res())

#for r in get_url():
#  print('这个for语句会提示语法错误')

Type Hinting

从3.5版本引入Type Hinting和typing 模块,支持类型声明,但仅仅用于声明,并不作任何实际的检查。声明的信息会保存在__annotations__中。3.6,3.7,3.8,3.9一直在完善该功能。

from __future__ import annotations
import typing

class A:

  # 3.5+ :支持type hinting
  def fn1(self, s: str) -> str:
    print(self.fn1.__annotations__)
    #实际上python不做任何检车,return None不会报错
    return None

  # 3.7+ : 支持postponed evaluation of type annotation
  # 尚未默认支持(预计3.10开启),需要 from __future__ import annotations
  def f(obj: B) -> str:
    return ''

class B:
    pass

a = A()
a.fn1('hehe')

# 3.6+ : 支持变量annotations
s : str
l : list = [1,2]
print(l)

# 3.7+ : 支持annotation 范型
# from 3.9 所有内置模块都支持annotation范型
l : list[int] = [1,2,3]
print(l)
def fp(l: typing.List[str], f: typing.Callable[str, str]) -> None:
    for i in l:
      print(f(i, 'c'))
fp(['a','b'], lambda x, y: x + '-' + y)

3.9 新语法

  • dict Union

    注意, a | b 和 b | a不同,后面的Value优先。

    | 只支持dict类型,但 |= 支持元组的list。

    >>> a = {'a': 1, 'b': 2}
    >>> b = {'a': 10, 'c': 20}
    >>> c = {'d': 9}
    >>> a | b
    {'a': 10, 'b': 2, 'c': 20}
    >>> b | a
    {'a': 1, 'c': 20, 'b': 2}
    >>> c |= a
    >>> c
    {'d': 9, 'a': 1, 'b': 2}
    >>> c | [('e', 6)]
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unsupported operand type(s) for |: 'dict' and 'list'
    >>> c |= [('e', 6)]
    >>> c
    {'d': 9, 'a': 1, 'b': 2, 'e': 6}
    >>>
    

3.8 新语法

  • Assignment Expressions

    赋值操作符 := ,其优先级非常低,需要注意。

    >>> if (d := 123) > 0: print(d)
    ...
    123
    >>> if d := 123 > 0: print(d)
    ...
    True
    
  • Postional-only Paramters

    参数列表中,/之前的参数,不能使用key=value形式,*之前的参数可以使用位置来标识,但*之后的参数必须使用key形式。

    >>> def f(a, /, b, *, c):
    ...   print(a, b, c)
    ...
    >>> f(1,2,3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() takes 2 positional arguments but 3 were given
    >>> f(1,2,c=3)
    1 2 3
    >>> f(1,b=2,c=3)
    1 2 3
    >>> f(a=1,b=2,c=3)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: f() got some positional-only arguments passed as keyword arguments: 'a'
    

3.6 新语法

  • f string

    前缀字符串增加f前缀,可以嵌套变量,3.8增加 “=” 的特性。

    >>> s = 'WW'
    >>> f = 7.159
    >>> a = {'h': 7}
    >>> f'3.6 support {s} {f:.1f} {a} {a["h"]} {{a}}'
    "3.6 support WW 7.2 {'h': 7} 7 {a}"
    >>> f'3.8 support: {f=} {f:.1f} {f=:.2f}'
    '3.8 support: f=7.159 7.2 f=7.16'
    >>> f'{math.cos(i)=:.2f} {[x for x in range(3)]} {type(a) is list}'
    'math.cos(i)=0.99 [0, 1, 2] False'
    
  • 数字类型支持下划线

    >>> i = 100_000_000_00
    >>> i
    10000000000
    >>> '{:_x}'.format(i)
    '2_540b_e400'
    >>> '{:_X}'.format(i)
    '2_540B_E400'
    >>> '{:_}'.format(i)
    '10_000_000_000'
    

3.5 新语法

  • @ 运算符

    3.5引入了新的运算符 @ ,用于矩阵乘积运算,但目前内置的所有数据类型都不支持这个运算符,可以通过___matmul__(), __rmatmul__(), __imatmul__()来重载 regular/reflected/in-place 矩阵运算。原来的numpy.dot函数可以直接用 @ 运算符来代替:

    >>> import numpy as np
    >>> x = np.ones(3)
    >>> m = np.eye(3)
    >>> x @ m
    array([1., 1., 1.])
    >>> np.dot(x, m)
    array([1., 1., 1.])
    
    class Pos:
      def __init__(self, x: int, y: int):
        self._x = x
        self._y = y
      def __str__(self):
        return '[' + str(self._x) + ', ' + str(self._y) + ']'
      def __matmul__(self, r: int):
        print('M')
        return Pos(self._x ** r, self._y ** r)
      def __rmatmul__(self, r: int):
        print('RM')
        return Pos(self._x ** r, self._y ** r)
        
      def __imatmul__(self, r: int):
        print('IM')
        self._x = self._x ** r
        self._y = self._y ** r
        return self
      
    p = Pos(2, 3)
    print(p @ 3) # call __matmul__
    print(4 @ p) # call __rmatmul__
    p @= 5       # call __imatmul__
    print(p)
    
  • unpacking扩展语法支持

    >>> a = [1,2,3]
    >>> b = {'a':3, 'b':4}
    >>> print(*a,*b)
    1 2 3 a b
    >>> def fn(a,b):
    ...   print(a,b)
    ...
    >>> fn(**b)
    3 4
    >>> *range(3),3
    (0, 1, 2, 3)
    >>> *range(3),
    (0, 1, 2)
    >>> [*range(4),4]
    [0, 1, 2, 3, 4]
    >>> [*range(4),4, *(4,5)]
    [0, 1, 2, 3, 4, 4, 5]
    >>> {'x':1, **{'y':2}}
    {'x': 1, 'y': 2}