python的基础知识其实不算多,也很简单,学会了string,list,dict数据结构和if for语法就能上手干很多事情了 ,所以很容易吸引一些对编程感兴趣想“速成”的入门学员,这两年渐渐成为了“网红”语言。但玩归玩闹归闹,别拿编程开玩笑,要想写出拿得出手的代码,很多看似不实用或很少考虑到的基础知识点还是要认真学习一下,不然很容易写出臃肿复杂低效的代码甚至在代码里埋下了很多雷。
下面就来总结一下python里容易被忽视的基础知识
1. __init__.py文件的作用
- 表示当前目录是python模块
- 初始化模块导入。基于这点可以用来定制模块导入行为
第一点,python代码如果放在文件里,那么必须要有__init__.py 文件才能从文件夹以外的代码里导入文件夹里的模块。
第二点,python里import 包时 会执行了包下面的__init__.py代码,如下结构中
test.py # from pac.pac2 import var
│
└─pac
│ __init__.py # print('first pac')
│
└─pac2
var.py
__init__.py # print('second pac')
#运行test.py 文件
python test.py
输出:
first pac
second pac
上面的例子说明导入模块时,经过的每一层的__init__.py文件都会执行。
基于这点,__init__ 文件可以用来控制模块的导入行为,
假如在上面的文件结构里,pac下有pac2, pac3... 文件夹,文件夹下也都有一个var.py文件,那么如果要使用这些var模块就得:
import pac
pac.pac2.var
pac.pac3.var
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'module' object has no attribute 'pac2'
#上面那样通过包名再访问包名的方式是常见的错误,正确的方式是:
from pac.pac2 import var
from pac.pac3 import var
...
如果包比较多这种写法很繁琐,这时候可以在pac包下的__init__.py文件下直接导入
# pac/__init__.py文件里
from pac2 import var as var2
from pac3 import var as var3
# 然后就可以这样调用
import pac
pac.var2
pac.var3
如果pac下有n 个pac子目录,那么在pac/__init__.py 写n个import语句还是很麻烦,其实还有一个方法,一次性导入pac下的所有模块,这就是__all__变量
# pac/__init__.py
__all__ = ['pac2', 'pac3']
#然后可以一次性导入pac2, pac3 子模块
from pac import *
pac2.var
pac3.var
其实这种用法在几乎所有的python模块里都使用了,比如经常用来爬虫的网络请求模块requests, 如果你用过这个包就会知道使用它的第一步就是import requests,然后发送get请求 requests.get, post请求 requests.post, 这里调用的get和post函数位置是在requests/api.py 文件里, 本应该这样调用 requests.api.get(这样调用确实也是可以的)。但是为什么可以直接通过requests调用呢?打开requests/__init__.py 文件里看到
...
from .api import request, get, head, post, patch, put, delete, options
...
这就是我们上面说的用法。这种用法很普遍也很便捷,以后可以在自己写的模块里这样用
2. 局部变量和全局变量
定义在函数内部的变量拥有一个局部作用域,定义在函数外的拥有全局作用域。
局部变量只能在其被声明的函数内部访问,而全局变量可以在整个程序范围内访问。
Python 中只有模块(module),类(class)以及函数(def、lambda)才会引入新的作用域,其它的代码块(如 if/elif/else/、try/except、for/while等)是不会引入新的作用域的,也就是说这些语句内定义的变量,外部也可以访问
变量的查找顺序为 局部变量—>全局变量—>内置变量
abs = 5 # 这个abs是全局变量
def some_func():
abs= 6 # 这个abs是局部变量
print(abs)
执行上面的例子,print(abs) 输出的是6,这个6就是局部变量,如果把abs=6 删除输出的就是5,这个5是全局变量,如果把abs=5也删掉,那么局部和全局空间里已经找不到变量abs,最后查找内置空间里的变量,而abs正好是python内置的计算绝对值的函数,则输出<built-in function abs>。
我们知道del var 可以删除变量var, 那这里可不可以把内置的函数变量也删掉呢?答案是不可以,del 可以删除局部和全局,不能删除内置变量
按照上面的变量查找顺序可知函数内部可以直接访问函数外面的全局变量,但是不可以更新外面的全局变量,如果需要更新,使用global关键字明确表示变量是全局的
a=1
def pt():
print(a)
pt()
#输出 1, 表示可以直接访问全局变量
def upd():
a=a+1
print(a)
upd()
# 更新变量会报错 UnboundLocalError: local variable 'a' referenced before assignment
def upd2():
global a
a=a+1
print(a)
#输出 2, 表示可以更新外部全局变量
def out():
a=1
def inner():
nonlocal a
a = 10
inner()
print(a)
# 输出10
#这种特殊情况,是内嵌的函数要访问外部函数的局部变量,可以直接访问,
#但是如果要更新不是使用global,因为global是函数外部的全局变量,
#而是使用另一个关键字nonlocal
注意变量的查找顺序不可逆,函数外面的全局变量是不可以访问函数内部的局部变量的
3. 运算符重载
加号(+)在python里有多种用法,用在两个数字上就是数学相加,用在两个字符串上就是字符串连接,用在两个列表上就是列表合并,这就是加号的多用情况。
python 类定义里有很多魔法函数,这些函数以双下划线开始和结尾,比如__init__, __new__,除此之外还有专门用于实现运算符重载的函数,比如上面的加号(+)对应的就是__add__函数,string和list 类就是重载了这个函数才会使得加号对于它们有特殊的意义。 我们在自己的类里面也能这么做
class A:
def __init__(self, a):
self.a = a
# adding two objects
def __add__(self, o):
return self.a + o.a
ob1 = A(1)
ob2 = A(2)
ob3 = A("Geeks")
ob4 = A("For")
print(ob1 + ob2)
print(ob3 + ob4)
#输出
3
GeeksFor
其实不仅加减乘除符号,大于小于等于这些比较符号都有对于的函数用来重载
4. 装饰器
装饰器是你提升python编程能力所必经之路,详细的介绍文章网上有很多,这里用我自己的话介绍一下,
装饰器的功能是 在不改变函数源码的情况下为其添加新的功能,使用到的技术是 函数里返回函数
例如想要给某个函数增加一个功能用来输出函数执行的时间:
import time
def deco(f):
def wraper():#deco 这个函数最终返回的是函数wraper,所以最终执行的是wraper这个函数
start=time.time()
f()
end=time.time()
print(end-start)
return wraper
#普通函数
def fun():
print('fun is running')
fun=deco(fun)#让同名变量fun指向wraper 函数,用新的fun覆盖老的fun来模拟装饰器功能
fun()
#装饰器函数
@deco
def fun2():
print('fun2 is running')
fun2()
上面两个函数的调用,结果一样的,fun()算是fun2()的通俗解释吧,把@deco 放在函数定义处的作用类似于fun=deco(fun)的写法,是python的一种语法糖,上面这是最基本的用法了,下面介绍几种高级一点的,实用一点的装饰器,包括:装饰有参数的函数的装饰器,自己也带有参数的装饰器,类装饰器 三种
4.1装饰有参数的函数的装饰器
def deco1(f):
def wraper(*args,**kwargs):#在wraper函数里定义两种可变参数用于接收任意f函数的任意参数
print('装饰器running......')
return f(*args,**kwargs)#注意,如果被装饰的函数有返回值,这里调用时要return结果
return wraper
@deco1
def added(a,b):
return a+b
4.2带有参数的装饰器
def deco2(prefix):
def wraper(f):
def inner_wraper(*args,**kwargs):
print('装饰器running......')
print('装饰器prefix:',prefix)
return f(*args,**kwargs)
return inner_wraper
return wraper
@deco2("I am prefix......")
def added(a,b):
return a+b
相比较于之前的装饰器,这里在最外面又套了一层函数,可以这样理解
added=deco2('I am prefix......')(added)
deco2('I am prefix......')返回wraper函数,然后再把added函数传入到wraper函数里,最终返回的就是包含added函数调用的inner_wraper函数了
4.3类装饰器
上面介绍的函数装饰器都有一个特点,那就是装饰器本身都是可调用的(callable),意思就是名字后面加上括号就能执行,比如deco(),但是一般的类对象是不行的,python类有一个内置方法call, 只要实现了这个方法那么这个对象就是可调用的,
class called:
def __call__(self):
print('call......')
c1=called()
c1()
>>call ......
有了上面这些,就让类装饰器变得可能了,
首先在类的构造函数init里接收一个函数,然后在call 函数里返回这个函数即可
class logging(object):
def __init__(self, func):
self.func = func
def __call__(self, *args, **kwargs):
print ("[DEBUG]: enter function {func}()".format(func=self.func.__name__))
return self.func(*args, **kwargs)
@logging
def say(something):
print ("say {}!".format(something))
怎么用函数的思想来理解上面的代码呢?
say=logging(say), 这里logging(say)返回的是类logging的一个实例,这个实例因为有call方法所以是可调用的,say()的本质就是执行logging类的call方法。
如果类装饰器也要带参数,那要怎么写呢?
这种情况下类的构造函数接收的不再是装饰的函数了,而是装饰器自己的参数,在call函数里接收要装饰的函数
class logging(object):
def __init__(self, level='INFO'):
self.level = level
def __call__(self, func): # 接受函数
def wrapper(*args, **kwargs):
print ("[{level}]: enter function {func}()".format(
level=self.level,
func=func.__name__))
func(*args, **kwargs)
return wrapper #返回函数
@logging(level='INFO')
def say(something):
print ("say {}!".format(something))
say=logging('INFO')(say)
logging('INFO')返回logging的实例,然后传入say函数作为参数调用这个实例,也就是调用实例的call方法,返回wrapper函数,这就又回到了上面的函数装饰器的形式
5. python闭包(closure)
我们知道python内嵌函数(nested function)可以直接访问包含着它的外部函数的变量的,但是外部函数的变量对于内嵌函数来说是只读的
def out():
a=1
def inner():
print(a)
inner()
out()
#输出
1
上面看到内嵌的inner函数是可以访问外部函数out的变量a,但是如果要在inner函数里修改变量a,需要使用nonlocal关键字,注意,nonlocal关键字只在python3 里有
上面的例子中,如果out函数最后不是调用inner函数,而是直接返回inner函数会怎样?
def out():
a=1
def inner():
print(a)
return inner
f=out()
f()
#输出
1
结果似乎如你预期,但是回头看看,在f=out()执行完后,out函数其实也已经执行完了,并返回了inner函数,
通过f()调用inner函数时,inner函数还是会记得a的值,甚至删除out函数,inner函数照样执行
del out
f()
#输出
1
这种数据绑定到代码的技术就是闭包(closure)
再看一个例子
def make_counter():
i = 0
def counter(): # counter() is a closure
nonlocal i
i += 1
return i
return counter
c1 = make_counter()
c2 = make_counter()
print (c1(), c1(), c2(), c2())
# 输出
1 2 1 2
实现闭包需要满足三个条件:
- 需要有一个内嵌函数
- 内嵌函数内部需要引用外部函数的变量
- 外部函数需要返回这个内嵌函数
闭包有很多用处,最常见的就是装饰器 decorator
5. 模块导入
如果我们知道需要导入的模块的位置,就可以使用import 关键字来导入,但有时候我们只知道模块的名字,也就是一个字符串形式的模块名,这要怎么导入呢?
方法有两种,一种是使用python 自带的importlib 包来实现,另一种是使用内置的__import__()方法来导入
代码目录结构如下:
# foo.py 文件
def add(a,b):
return a+b
# test.py 文件
# 1.使用importlib
import importlib
module = importlib.import_module("lib.foo")
print(module.add(1, 2))
#2. 使用__import__
module = __import__('lib.foo', fromlist=['lib', ])
print(module.add(7, 8))