最近在补 Python 进阶的内容,学习资源来自:Python 进阶,是《Intermediate Python》的中译本。这里面的一些内容,对于我来说是比较容易忽略的知识点。趁最近心态涣散,整理一些相对比较简单的内容。
*args 和 **kwargs
*args
和 **kwargs
这对老兄弟,经常结伴出现。它们主要用于函数定义,可以将未知数量的参数传递给一个函数。*arg
传递的是非键值对的参数列表,而 **kwargs
用于传递键值对参数列表。下面是几个例子:
-
*args
的例子:
In [2]: def test_var_arg(f_arg, *args):
...: print("first normal arg:", f_arg)
...: for arg in args:
...: print("another arg through *argv:", arg)
...:
In [3]: test_var_arg("cao", "qi", "95")
first normal arg: cao
another arg through *argv: qi
another arg through *argv: 95
-
**kwargs
的例子:
In [5]: def greet_me(**kwargs):
...: for key , value in kwargs.items():
...: print("{0} == {1}".format(key, value))
...:
In [6]: greet_me(name="caoqi95")
name == caoqi95
- 使用场景及顺序:
# 使用 *args
>>> args = ("two", 3, 5)
>>> test_args_kwargs(*args)
arg1: two
arg2: 3
arg3: 5
# 使用 **kwargs:
>>> kwargs = {"arg3": 3, "arg2": "two", "arg1": 5}
>>> test_args_kwargs(**kwargs)
arg1: 5
arg2: two
arg3: 3
# 如果想在一个函数里,同时使用标准参数,*args 和 **kwargs,顺序如下:
function_name(normal_arg, *args, **kwargs)
装饰器(Decorator)
在 Python 中,函数可以作为参数被另一个函数所调用。我们先看看函数作为参数被调用的例子,然后再讲装饰器的内容,这样会比较容易理解。
In [11]: def hi():
...: return "hi, caoqi95"
...:
...:
In [12]: def doSomeThingBeforeHi(func):
...: print("I am doing some boring work before executing hi() function")
...: print(func())
...:
In [13]: doSomeThingBeforeHi(hi)
I am doing some boring work before executing hi() function
hi, caoqi95
上面的例子中,就是把函数 hi
作为参数,然后被函数 doSomeThingBeforeHi(func)
调用。其实,装饰器起到的作用也是实现一个函数作为参数被另一个函数调用的功能,即给当前函数增加额外的一些功能。现在,将上面的代码写成装饰器的形式:
In [7]: def doSomeThingBeforeHi(func):
...: def call():
...: print("I am doing some boring work before hi() function")
...: print(func())
...: return call
...:
In [8]: def hi():
...: return "hi, caoqi95"
...:
In [9]: hi = doSomeThingBeforeHi(hi) # 用 doSomeThingBeforeHi 函数装饰 hi 函数
In[10]: hi()
I am doing some boring work before hi() function
hi, caoqi95
在上面的代码中,hi
函数被当做参数,赋值给了 doSomeThingBeforeHi
函数。然后在 hi
变量后面加上一对小括号执行函数。整个结合起来,可以看出,hi
函数被装饰了,多出了额外的功能。下面再写成专业一点的装饰器,用 @
来表示:
In [1]: def doSomeThingBeforeHi(func):
...: def call():
...: print("I am doing some boring work before hi() function")
...: print(func())
...: return call
...:
...: @doSomeThingBeforeHi
...: def hi():
...: return "hi, caoqi95"
In [2]: hi()
I am doing some boring work before hi() function
hi, caoqi95
改写完成,可以发现 @doSomeThingBeforeHi
取代了 hi = doSomeThingBeforeHi(hi)
这行代码,使代码变得简洁,符合 Python 的核心价值观。但是,上面的装饰器还会存在一点小问题:
In [3]: print(hi.__name__)
call
运行完发现,hi()
函数的名字从 hi
变成了 call
。在 Python 中,可以用 wraps
函数解决这个问题,如下所示:
from functools import wraps
def doSomeThingBeforeHi(func):
@wraps(func)
def call():
print("I am doing some boring work before hi() function")
print(func())
return call
@doSomeThingBeforeHi
def hi():
return "hi, caoqi95"
在这里也可以发现 wraps()
也是一个装饰器。总结到这里,你也可以试着自己写一个装饰器,不会很难的。
对象可变与不可变
Python 中的数据类型包含可变(mutable)与不可变(immutable),有时候会让人很头疼。
首先让我们来看一个例子:
>>> before = ['hi']
>>> after = before
>>> after += ['hello']
>>> before
['hi', 'hello']
>>> after
['hi', 'hello']
再来看一个例子:
>>> before = "hi"
>>> after = before
>>> after += "hello"
>>> before
'hi'
>>> after
'hihello'
我们对第一个例子的预期应该同第二个例子一样,before
变量前后的值是不变的,只有 after
的值会变化:
>>> before = ['hi']
>>> after = before
>>> after += ['hello']
>>> before
['hi']
>>> after
['hi', 'hello']
但是,实际结果并不符合预期,这是为什么?因为 Pyhton 中对象可变性在作祟。什么是对象可变?就是每当将一个变量赋值为另一个可变类型的变量时,对这个数据的任意改动会同时反映到这两个变量上去。
这种情况只是针对可变数据类型。列表(list)是属于可变类型的,因此 ,第一个例子中的before
变量前后的值是不一样的。而字符串(string)是属于不可变类型的,所以,第二个例子中的 before
变量的值前后不变。
下面再举一个例子说明,这样可以更理解对象可变这个概念,同时又能够避免一些问题。
def add_to(num, target=[]):
target.append(num)
return target
>>> add_to(1)
[1]
>>> add_to(2)
[1, 2]
>>> add_to(3)
[1, 2, 3]
你期待的函数的表现应该是这个样子的:
def add_to(num, target=[]):
target.append(num)
return target
>>> add_to(1)
[1]
>>> add_to(2)
[2]
>>> add_to(3)
[3]
这也是因为列表是可变对象的原因。上面的函数还暴露出一个问题,即在 Python 中,当函数被定义时,默认参数只会运算一次,而不是每次被调用时都会重新运算。所以,应该避免定义可变类型的参数。如果希望每次调用函数的时候,默认函数都重新运算,那么上面的函数可以改写成下面的形式:
def add_to(num, target=None):
if target == None:
target = []
target.append(num)
return target
>>> add_to(1)
[1]
>>> add_to(2)
[2]
>>> add_to(3)
[3]
最后,总结一下 Python 中哪些对象可变,哪些对象不可变。
- 可变对象:
dict
,list
- 不可变对象:
string
,int
,float
,tuple
slots 魔法
在 Python 中,每个类都有实例属性。在默认的情况下,会用一个字典来保存一个对象的实例属性。但是,有时候这样会浪费很多内存。尤其是在创建的对象的规模十分大的时候,会非常消耗内存。这是因为 Python 不能在对象创建的时候,直接分配一个固定的内存来保存所有的属性。
但是,可以使用 __slots_
来解决消耗内存的问题。__slots__
会告诉 Python 不要使用字典,而且只给一个固定集合的属性分配空间。可以看看下面两个例子:
-
不使用
__slots__
:class Myclass(object): def __init__(self, name, identifier): self.name = name self.identifier = identifier
-
使用
__slots__
:class Myclass(object): __slots__ = ['name', 'identifier'] def __init__(self, name, identifier): self.name = name self.identifier = identifier
下面可以通过使用 Ipython
的扩展模块 ipython_memory_usage,来查看内存的使用情况。
首先,安装这个模块:
pip install ipython_memory_usage
安装成功后,在命令行窗口输入 ipython
(提前已安装好 ipython
模块),来开启 IPython
模式。
E:\Python>ipython
Python 3.6.5 |Anaconda, Inc.| (default, Mar 29 2018, 13:32:41) [MSC v.1900 64 bit (AMD64)]
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.
In [1]: import ipython_memory_usage.ipython_memory_usage as imu
In [2]: imu.start_watching_memory()
In [2] used 0.3945 MiB RAM in 35.71s, peaked 0.00 MiB above current, total RAM usage 40.01 MiB
In [3]: from slots import Myclass as My1
In [3] used 0.0469 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 40.05 MiB
In [4]: num = 1024*256
In [4] used 0.0039 MiB RAM in 0.10s, peaked 0.00 MiB above current, total RAM usage 40.06 MiB
In [5]: x = [My1(1,1) for i in range(num)]
In [5] used 16.1797 MiB RAM in 0.28s, peaked 0.00 MiB above current, total RAM usage 56.24 MiB
In [6]: from no_slots import Myclass as My2
In [6] used -0.0039 MiB RAM in 0.11s, peaked 0.00 MiB above current, total RAM usage 56.23 MiB
In [7]: x = [My2(1,1) for i in range(num)]
In [7] used 28.6367 MiB RAM in 0.31s, peaked 0.00 MiB above current, total RAM usage 84.87 MiB
首先在 E:\Python
的目录下,创建 slots.py
和 no_slots.py
这两个文件,内容和上面两个代码块的内容一样。然后再开启 Ipython
模式,运行查看个别的占用内存情况。可以发现,no_slots.py
的情况,占用了 28.6 MB;而 slots.py
的情况,占用了 16.2 MB。相比之下,减少了不少内存。
网友评论