最近阅读《Python工匠:案例、技巧与工程实践》发现一些零零碎碎的之前不知道的知识点,因此总结记录方便以后翻找查阅。

变量与注释

变量解包是python里一种特殊的赋值操作,允许把可迭代的对象所有成员一次性赋值给多个变量。

1
2
3
4
5
6
>>> usernames=['a','b']
>>> a,b = usernames 


>>> attrs=[1,['a','b']]
>>> n,(a,b) = attrs 

此外特殊技巧,比如利用星号表达式贪婪的获取多个数据。

1
2
3
4
>>> data = ['a','b','c','d','e']
>>> a,*b,c = data
>>> print(b)
['b','c','d']

单下划线在常用的诸多变量名中,单下划线_是比较特殊的一个。它常作为一个无意义的占位符出现在赋值语句中。需要注意的是在Python交互式命令行里,_变量还有一层特殊含义——默认保存我们输入的上个表达式的返回值。

1
2
3
4
5
>>> foo = ['a','b','c']
>>> a,_,c = foo

>>> data = ['a','b','c','d','e']
>>> a,*_,c = data

数值与字符串

当前的主流Python版本中,至少有三种主要的字符串格式化方式。

  • C语言风格的基于百分号%的格式化语句:‘Hello, %s’ % ‘World’。
  • 新式字符串格式化(str.format)方式(Python 2.6新增):“Hello,{}".format (‘World’)。
  • f-string字符串字面量格式化表达式(Python 3.6新增):name = ‘World’;f’Hello, {name}’。

具体使用方法参考官方文档,日常编码中推荐优先使用f-string,搭配str.format作为补充,想必能满足绝大多数的字符串格式化需求。

广义上的“字符串”概念可分为两类。

  • 字符串:我们最常挂在嘴边的“普通字符串”,有时也被称为文本(text),是给人看的,对应Python中的字符串(str)类型。str使用Unicode标准,可通过.encode()方法编码为字节串。
  • 字节串:有时也称“二进制字符串”(binary string),是给计算机看的,对应Python中的字节串(bytes)类型。bytes一定包含某种真正的字符串编码格式(默认为UTF-8),可通过.decode()解码为字符串。
1
2
3
4
5
6
>>> str_obj = 'Hello, 世界'
>>> type(st_obj)
<class 'str'>
>>> bin_obj = str_obj.encode('UTF-8')
>>> type(bin_obj)
<class 'bytes'>

因为字符串面向的是人,而二进制的字节串面向的是计算机,因此,在使用体验方面,前者要好得多。在我们的程序中,应该尽量保证总是操作普通字符串,而非字节串。必须操作处理字节串的场景,一般来说只有两种:

  • 程序从文件或其他外部存储读取字节串内容,将其解码为字符串,然后再在内部使用;
  • 程序完成处理,要把字符串写入文件或其他外部存储,将其编码为字节串,然后继续执行其他操作。

当把字符串写入文件时,请谨记:普通字符串采用的是文本格式,没法直接存放于外部存储,一定要将其编码为字节串——也就是“二进制字符串”——才行。这个编码工作有时需要显式去做,有时则隐式发生在程序内部。比如在写入文件时,只要通过encoding参数指定字符串编码格式,Python就会自动将写入的字符串编码为字节串。

1
2
3
with open('test.txt','w','UTF-8') as fp:
    str_obj = 'abc'
    fp.write(str_obj)

如果不指定encoding参数,Python会尝试自动获取当前环境下偏好的编码格式。

1
2
3
>>> import locale
>>> locale.getpreferredencoding()
'UTF-8'

容器

列表推导式把几类操作压缩在了一起,结果就是:代码量更少,并且维持了很高的可读性。因此,列表推导式可以算得上处理列表数据的一把“利器”。

1
2
numbers = [1,2,3,4,5,6,7,8]
results = [n*100 for n in numbers if n % 2 == 0]

Python里的内置数据类型,大致上可分为可变与不可变两种。

  • 可变(mutable):列表、字典、集合。
  • 不可变(immutable):整数、浮点数、字符串、字节串、元组。

Python在进行函数调用传参时,采用的既不是值传递,也不是引用传递,而是传递了“变量所指对象的引用”(pass-by-object-reference)。换个角度说,当我们调用函数将外部变量作为参数传递进来后,Python会新建了一个函数内部变量,然后让它和外部变量指向同一个对象,相当于做了一次变量赋值。而之后的函数内部处理是否会影响参数的数据,则取决于对象的可变性。

字典在范问不存在的键时,会抛出异常,这样便引发很多情景,例如获取、新增、删除等操作都需要考虑键值不存在的情况,接下来我们介绍几种字典方法,避免了冗余的异常捕获,方便操作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 使用get方法,没有键可以返回指定默认值
movie.get('rating',0)

// 使用setdefault方法,没有键则新建
>>> d = {'a':'1'}
>>> d.setdefault('b',[]).append('2')
>>> print(d)
{'a':'1','b':['2']}

// 使用pop方法,键不存在不会抛异常,记得传默认值
d.pop(key,None)

自python3.6开始字典的底层实现改变了,导致具有了有序性,即当你按照某种顺序把内容存进字典后,可以按照原顺序把它取出来了。

还需要注意的是内容一致而顺序不同的字典被视作相等,因为解释器只对比字典的键和值是否一致。

集合中只能存放可以hash的对象,某种类型是否可哈希遵循下面的规则

  • 所有的不可变内置类型,都是可哈希的,比如str、int、tuple、frozenset等;
  • 所有的可变内置类型,都是不可哈希的,比如dict、list等;
  • 对于不可变容器类型(tuple, frozenset),仅当它的所有成员都不可变时,它自身才是可哈希的;
  • 用户定义的类型默认都是可哈希的。谨记,只有可哈希的对象,才能放进集合或作为字典的键使用。
1
2
3
4
>>> hash('string')
-340728631374970639
>>> hash(100)
100

条件分支

当我们需要判断两个对象是否相等时,通常会使用双等号运算符==,它会对比两个值是否一致,然后返回一个布尔值结果。

对于自定义对象来说,它们在进行==运算时行为是可操纵的:只要实现类型的__eq__魔法方法就行。

如何严格检查某个对象是否为None呢?答案是使用is运算符。虽然二者看上去差不多,但有着本质上的区别:

  • ==对比两个对象的值是否相等,行为可被__eq__方法重载。
  • is判断两个对象是否是内存里的同一个东西,无法被重载。

因此在执行x is y时,其实就是在判断id(x)和id(y)的结果是否相等,二者是否是同一个对象。

既然is在进行比较时更严格,为什么不把所有相等判断都用is来替代呢?这是因为,除了None、True和False这三个内置对象以外,其他类型的对象在Python中并不是严格以单例模式存在的。

1
2
3
4
5
6
7
8
>>> x = 23000
>>> y = 23000
>>> x is y
False
>>> id(x),id(y)
(123456789,432156789)
>>> x == y
True

上面的例子需要注意,如果把23000改成100,则x is y会判断为True,这是因为Python语言使用了一种名为“整型驻留”(integer interning)的底层优化技术。对于从-5到256的这些常用小整数,Python会将它们缓存在内存里的一个数组中。当你的程序需要用到这些数字时,Python不会创建任何新的整型对象,而是会返回缓存中的对象。这样能为程序节约可观的内存。

装饰器

装饰器是一种通过包装目标函数来修改其行为的特殊高阶函数,绝大多数装饰器是利用函数的闭包原理实现的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def timer(func):
    """装饰器:打印函数耗时"""
    def decorated(*args, **kwargs):
        st = time.perf_counter()
        ret = func(*args,**kwargs)
        print('time cost:{} seconds'.format(time.perf_counter() - st))
        return st
    return decorated

// 使用timer装饰器

@timer
def random_sleep():
    time.sleep(random.random())

如果一个类实现了__call__魔法方法,那么它的实例也会变成可调用对象。如果一个类实现了__call__魔法方法,那么它的实例也会变成可调用对象。

1
2
3
4
5
6
7
8
9
>>> class Foo:
...     def __call__(self, name):
...         print(f'hello,{name}')
...
>>> foo=Foo()
>>> callable(foo)
True
>>> foo('world')
hello,world

书里面写的内容看着蛮复杂,后期专门整理博客介绍。

面向对象

在Python里,所有的类属性和方法默认都是公开的,不过我们可以通过添加双下划线前缀__的方式把它们标示为私有。

1
2
3
4
5
6
7
class Foo:
    def __init__(self):
        self.__bar = 'baz'

>>> foo = Foo()
>>> foo.__bar
AttributeError: 'Foo' object has no attribute '__bar'

代码中Foo类的bar就是一个私有属性,如果尝试从外部访问它,程序就会抛出异常。虽然上面是设置私有属性的标准做法,但Python里的私有只是一个“君子协议”。“君子协议”是指,虽然用属性的本名访问不了私有属性,但只要稍微调整一下名字,就可以继续操作__bar

1
2
>>> foo._Foo__bar
'baz'

这是因为当我们使用__{var}的方式定义一个私有属性时,Python解释器只是重新给了它一个包含当前类名的别名_{class}__{var},因此你仍然可以在外部用这个别名来访问和修改它。

总结

开始浏览了一遍,其中较多设计修饰器,这个后期再仔细学习一下。

参考