本文假设你已经有一门面向对象编程语言基础,如
Java
等,且希望快速了解并使用
Python
语言。本文对重点语法和数据结构以及用法进行详细说明,同时对一些难以理解的点进行了图解,以便大家快速入门。一些较偏的知识点在大家入门以后根据实际需要再查询官方文档即可,学习时切忌胡子眉毛一把抓。同时,一定要跟着示例多动手写代码。学习一门新语言时推荐大家同时去刷
leetcode
,一来可以快速熟悉新语言的使用,二来也为今后找工作奠定基础。推荐直接在网页上刷
leetcode
,因为面试的时候一般会让你直接在网页编写代码。
leetcode
刷题路径可以按我推荐的方式去刷。以下代码中,以
>>>
和
...
开头的行是交互模式下的代码部分,
>?
开头的行是交互模式下的输入,其他行是输出。
python
代码中使用
#
开启行注释。
模块
模块是包含
Python
定义和语句的文件,文件后缀名为
.py
,文件名即是模块名。在
pycharm
中创建名为
python-learn
的项目,然后创建
fib.py
文件,并输入以下代码后保存:
# 斐波拉契数列def print_fib(n: int) -> None:"""打印斐波拉契数列:param n: 数列截至范围:return: None"""a, b = 0, 1while a < n:print(a, end=\' \')a, b = b, a+bprint()def get_fib(n: int) -> list:"""获取斐波拉契数列:param n: 数列截至范围:return: 存有数列的list"""result = []a, b = 0, 1while a < n:result.append(a)a, b = b, a+breturn result
接着,打开
pycharm
中的
Python Console
,使用
import
语句导入该模块:
# 导入 fib 模块>>> import fib# 使用 fib 模块中定义的函数>>> fib.print_fib(10)0 1 1 2 3 5 8>>> fib.get_fib(10)[0, 1, 1, 2, 3, 5, 8]# 如果经常使用某个函数,可以把它赋值给局部变量>>> print_fib = fib.print_fib>>> print_fib(10)0 1 1 2 3 5 8# 每个模块中都有一个特殊变量 __name__ 记录着模块的名字>>> print(fib.__name__)fib
使用
as
关键字为导入的模块指定别名:
# 导入 fib 并指定别名为 fibonacci>>> import fib as fibonacci>>> fibonacci.get_fib(10)[0, 1, 1, 2, 3, 5, 8]# 模块名依然为 fib>>> print(fibonacci.__name__)fib
当我们通过
import
语句导入模块时:
- 首先查找要导入的模块是否为内置模块;
- 没找到就会根据
sys.path
(list)中的路径继续查找。(
sys.path
默认值包含:当前路径、环境变量
PYTHONPATH
中的路径等)
查看
sys.path
的值:
>>> print("======= start =======")... for path in sys.path:... print(path)... print("======= end =======")======= start =======C:\\software\\jetbrains\\PyCharm 2021.2.3\\plugins\\python\\helpers\\pydevC:\\software\\jetbrains\\PyCharm 2021.2.3\\plugins\\python\\helpers\\pycharm_displayC:\\software\\jetbrains\\PyCharm 2021.2.3\\plugins\\python\\helpers\\third_party\\thriftpyC:\\software\\jetbrains\\PyCharm 2021.2.3\\plugins\\python\\helpers\\pydevC:\\software\\anaconda3\\envs\\python-learn\\python310.zipC:\\software\\anaconda3\\envs\\python-learn\\DLLsC:\\software\\anaconda3\\envs\\python-learn\\libC:\\software\\anaconda3\\envs\\python-learnC:\\software\\anaconda3\\envs\\python-learn\\lib\\site-packagesC:\\software\\jetbrains\\PyCharm 2021.2.3\\plugins\\python\\helpers\\pycharm_matplotlib_backendD:\\code\\python\\python-learn # fib 模块所在目录D:/code/python/python-learn # 对应linux路径形式======= end =======
当我们要导入的模块路径不在
sys.path
中时,通过
append
或
extend
函数可以将目标路径手动加入
sys.path
中。前面的例子里,为什么我们没有手动将项目路径加入
sys.path
中就可以导入
fib
模块呢?答案是
pycharm
帮我们做了。在项目中,当我们打开
Python Console
时,
pycharm
执行了以下脚本:
sys.path.extend([\'D:\\\\code\\\\python\\\\python-learn\', \'D:/code/python/python-learn\'])
**注意:**为了保证运行效率,每次解释器会话只导入一次模块。如果更改了模块内容,必须重启解释器;仅交互测试一个模块时,也可以使用
importlib.reload()
,例如
import importlib; importlib.reload(modulename)
。
以脚本方式执行模块
.py
文件(模块)还可以通过
python
解释器以脚本的方式执行。在
pycharm
项目中打开
Terminal
并执行以下命令可以解释执行
fib
模块(也可点击图形界面的执行按钮):
python fib.py
执行
fib.py
时,解释器从上到下依次解释执行,由于代码中没有任何输出动作所以终端没有任何输出。
当一个
.py
文件(模块)被当作脚本执行时,会被认为是程序的入口,类似于其他语言中的
main
函数,于是
python
解释器会将该模块的特殊变量
__name__
置为
__main__
。
现在,我们给
fib.py
文件添加一些内容:
# 斐波拉契数列def print_fib(n: int) -> None:"""打印斐波拉契数列:param n: 数列截至范围:return: None"""a, b = 0, 1while a < n:print(a, end=\' \')a, b = b, a+bprint()def get_fib(n: int) -> list:"""获取斐波拉契数列:param n: 数列截至范围:return: 存有数列的list"""result = []a, b = 0, 1while a < n:result.append(a)a, b = b, a+breturn result# 只有当前文件(模块)被当作脚本执行时 if 语句才为真if __name__ == "__main__":import sys# argv[0]始终为文件名print(sys.argv[0], end=" ")# 传入的第一个参数print(sys.argv[1], end=" ")# 传入的第二个参数print(sys.argv[2])# 测试 print_fib 函数print_fib(int(sys.argv[1]))# 测试 get_fib 函数fibSeries = get_fib(int(sys.argv[2]))print(fibSeries)
输入以下命令并执行:
PS D:\\code\\python\\python-learn> python fib.py 10 20fib.py 10 200 1 1 2 3 5 8[0, 1, 1, 2, 3, 5, 8, 13]
包
如果你了解
Java
,那么
Python
中的包你就不会感到陌生。包是命名空间的一种实现方式,不同包中的同名模块互不影响。包的写法类似于
A.B.C
,
A
是包,
B
是
A
的子包,
C
可以是B的子包也可以是模块。包在磁盘上的表现就是目录或者说是路径,以包结构
A.B.C
为例,若
C
为模块,那么对应的路径为
项目路径/A/B/C.py
。同时
Python
只把含
__init__.py
文件的目录当成包。(后面解释这个文件的用处)
以之前创建的
python-learn
项目为例,在根目录下创建包
com.winlsr
,然后将
fib.py
移动到
com.winlsr
下,目录结构如下:
从包中导入
fib
模块:
# 导入 fib 模块>>> import com.winlsr.fib# 使用时必须引用模块的全名>>> com.winlsr.fib.print_fib(10)0 1 1 2 3 5 8
使用
from package import ...
导入模块、函数(建议重启一下解释器):
# 导入 fib 模块>>> from com.winlsr import fib# 使用时直接输入模块名>>> fib.print_fib(10)0 1 1 2 3 5 8# 直接导入 fib 模块中的 print_fib 函数>>> from com.winlsr.fib import print_fib# 直接使用方法即可>>> print_fib(10)0 1 1 2 3 5 8
使用
from package import item
时,item 可以是包的子模块(或子包),也可以是包中定义的函数、类或变量等其他名称。使用
import item
时,
item
只可以是模块或包。有的同学会疑问,导入包有什么用?以当前项目为例,我们导入
com
包(建议重启一下解释器):
# 导入 com 包>>> import com>>> com# 包其实也是模块,对应的文件是包下的 __init__.py 文件?<module \'com\'="" from="" \'d:\\\\code\\\\python\\\\python-learn\\\\com\\\\__init__.py\'=""># 验证猜想:# 将该 __init__.py 文件中添加如下内容def print_info():print("name :", __name__)# 重启解释器后再次导入 com 包>>> import com# 成功调用 __init__.py 文件中定义的函数,猜想正确>>> com.print_info()name : com
从包中导入 *
from package import *
不会导入
package
中的任何模块或子包,除非你在该
package
下的
__init__.py
文件中添加了如下显示说明:
__all__ = ["子模块名1", "子包名1"]
添加该说明后,执行
from package import *
语句会导入指定的子模块、子包。
同样以之前创建的
python-learn
项目为例,我们执行如语句(建议重启一下解释器):
# 希望导入 com.winlsr 包下的 fib 模块>>> from com.winlsr import *# 发现并没有导入>>> fibTraceback (most recent call last):File "<input>", line 1, in <module>NameError: name \'fib\' is not defined
在
com.winlsr
下的
__init__.py
文件中添加如下内容:
__all__ = ["fib"]
执行同样的语句(建议重启一下解释器):
>>> from com.winlsr import *# 导入成功>>> fib<module \'com.winlsr.fib\'="" from="" \'d:\\\\code\\\\python\\\\python-learn\\\\com\\\\winlsr\\\\fib.py\'="">
注意:通常不建议采用该小结讲解的方法导入模块或子包,而是采用
from package import specific_submodule
。
dir()
函数
内置函数
dir()
用于查找模块或子包中定义的名称(变量、模块(子包)、函数等),返回结果是经过排序的字符串列表。没有参数时,
dir()
列出当前定义的名称。
>>> from com.winlsr import fib>>> dir(fib)[\'__builtins__\', \'__cached__\', \'__doc__\', \'__file__\', \'__loader__\', \'__name__\', \'__package__\', \'__spec__\', \'get_fib\', \'print_fib\']>>> dir()[\'__builtins__\', \'__doc__\', \'__file__\', \'__loader__\', \'__name__\', \'__package__\', \'__spec__\', \'fib\', \'sys\']
类
封装
Python
中定义类使用
class
关键字,类中可以定义变量和函数,起到封装的作用。沿用之前创建的项目,在
com.winlsr
包中创建
rectangle
模块并定义矩形类:
class Rectangle:# 一个类只能有一个构造函数def __init__(self): # 无参构造,self 类似于 this,指向实例对象本身self.length = 0.0 # 构造函数中对实例对象的成员变量width和length进行初始化self.width = 0.0 # 只有创建的实例才有这两个变量# 计算面积def area(self):return self.length * self.width# 计算周长def perimeter(self):return (self.length + self.width) * 2
打开
Python Console
,输入以下语句来使用
Rectangle
类:
>>> from com.winlsr.rectangle import Rectangle# 使用无参构造创建类的实例对象>>> rec = Rectangle()# 调用rec实例的方法>>> rec.perimeter()0.0>>> rec.area()0.0>>> rec.length0.0# Rectangle 类中没有length这个成员变量,只有实例对象中才有>>> Rectangle.lengthTraceback (most recent call last):File "<input>", line 1, in <module>AttributeError: type object \'Rectangle\' has no attribute \'length\'
Python
中实例对象的成员变量也可以不在
__init__()
中定义和初始化,而是直接在类中定义并初始化。原来的
Rectangle
类可以改写为:
class Rectangle:# 成员变量直接定义在类中并初始化,类和实例都有两个变量length = 0.0width = 0.0def area(self):return self.length * self.widthdef perimeter(self):return (self.length + self.width) * 2
注意,如果变量直接定义在类中,那么创建实例时,实例中的变量是类中变量的浅拷贝。重启解释器执行以下语句:
>>> from com.winlsr.rectangle import Rectangle>>> rec0 = Rectangle()>>> rec0.width is Rectangle.widthTrue>>> rec1 = Rectangle()>>> rec1.length is Rectangle.lengthTrue
实例
rec0
和
rec1
创建后的内存分布如下:
对实例
rec0
和
rec1
中的成员变量进行修改:
# 修改实例的成员变量>>> rec0.width = 1.0... rec0.length = 2.0... rec1.width = 3.0... rec1.length = 4.0# 打印>>> print(Rectangle.width)... print(Rectangle.length)... print(rec0.width)... print(rec0.length)... print(rec1.width)... print(rec1.length)0.00.01.02.03.04.0
根据结果可以发现
rec0
和
rec1
实例之间的成员变量(不可变 immutable 类型)相互之间不影响,修改后它们的内存分布如下:
注意,如果直接定义在类中的变量是可变(mutable)类型,使用时就应该谨慎。如下,依然在
com.winlsr
包中创建
actor
模块并定义演员类:
class Actor:# 参演的电影movies = []def get_movies(self):return self.moviesdef add_movie(self, movie):self.movies.append(movie)
使用
Actor
类:
>>> from com.winlsr.actor import Actor... lixiaolong = Actor()... xuzheng = Actor()... lixiaolong.add_movie("猛龙过江")... xuzheng.add_movie("我不是药神")... print(lixiaolong.get_movies())... print(xuzheng.get_movies())# 发现两个对象的 movies list 是共享的[\'猛龙过江\', \'我不是药神\']print(xuzheng.get_movies())[\'猛龙过江\', \'我不是药神\']
以上情况是因为多个
Actor
类的实例中的
movies
变量(引用)指向同一个
list
对象,实例
lixiaolong
和
xuzheng
内存分布如下:
创建后实例后,调用
add_movie()
之前的内存分布:
调用
add_movie()
之后的内存分布:
为了使每个演员的参演电影列表相互独立,在创建
Actor
类的实例时应该为每个实例创建一个新的
list
:
class Actor:# 参演的电影# 该语句没有用,可以删掉。实例在创建时会通过构造函数改变指向movies的指向movies = []def __init__(self):self.movies = []def get_movies(self):return self.moviesdef add_movie(self, movie):self.movies.append(movie)
重启解释器,再次执行以下语句:
>>> from com.winlsr.actor import Actor... lixiaolong = Actor()... xuzheng = Actor()... lixiaolong.add_movie("猛龙过江")... xuzheng.add_movie("我不是药神")... print(lixiaolong.get_movies())... print(xuzheng.get_movies())[\'猛龙过江\'][\'我不是药神\']
综上,根据前面的示例,如果你不希望多个实例之间共享变量,建议直接将变量定义在
__init__
函数中。最后,
Python
支持静态语言不支持的实例属性动态绑定特性:
# 给 lixiaolong 这个实例动态添加 age 属性>>> lixiaolong.age = 41>>> lixiaolong.age41# 不影响其他实例>>> xuzheng.ageTraceback (most recent call last):File "<input>", line 1, in <module>AttributeError: \'Actor\' object has no attribute \'age\'
访问控制
前面的实例中,我们可以通过
instance.var
的形式来直接访问实例的成员变量。但通常我们不希望实例中的成员变量被直接访问,而是通过
getter
和
setter
来访问,这需要我们将成员变量设置为
private
。
Python
中:
- 带有一个下划线的变量,形如
_var
应该被当作是
API
(常用于模块中)的非公有部分 (函数或是数据成员)。虽然可以正常访问,但我们应遵循这样一个约定;
- 类中私有成员变量应当用两个前缀下划线,至多一个后缀下划线标识,形如:
__var
。但该变量并不是真正的不能访问,这是因为
Python
实现的机制是”名称改写“。这种机制在执行时会将
__var
改为
_classname__var
,但你仍然可以通过
_classname__var
来访问。
- 形如
__var__
的变量是特殊变量,可以访问,但通常我们不需要定义此类变量。
实验验证,将
Rectangle
类改为如下代码:
class Rectangle:def __init__(self, length, width):self.__length = lengthself.__width = widthdef area(self):return self.__length * self.__widthdef perimeter(self):return (self.__length + self.__width) * 2
重启解释器,执行如下语句:
>>> from com.winlsr.rectangle import Rectangle>>> rec = Rectangle(12.0, 24.0)# 直接访问私有变量 __width,失败>>> print(rec.__width)Traceback (most recent call last):File "<input>", line 1, in <module>AttributeError: \'Rectangle\' object has no attribute \'__width\'# 访问私有变量改写后的名称 _Rectangle__width,成功>>> print(rec._Rectangle__width)24.0
对应
Python Console
如下:
继承
Python
继承语法如下,所有类都默认继承:
class DerivedClassName(BaseClassName):<statement-1>...<statement-n>
下面我们在
com.winlsr
包下创建
person
模块并定义
Person
类,作为
Actor
和
Teacher
类的基类:
class Person:def __init__(self, name, id_number):self.__name = nameself.__id_number = id_numberdef set_name(self, name):self.__name = namedef get_name(self):return self.__namedef set_id_number(self, id_number):self.__id_number = id_numberdef get_id_number(self):return self.__id_number
在
com.winlsr
包下创建
actor
模块并定义
Actor
类,它是
Person
类的派生类:
from .person import Personclass Actor(Person):def __init__(self, name, id_number):self.__movies = []# 三种调用父类构造函数的方式super().__init__(name, id_number)# super(Actor, self).__init__(name, id_number)# Person.__init__(self, name, id_number)def add_movie(self, movie):self.__movies.append(movie)def print_info(self):print(self.get_name(), self.get_id_number(), self.__movies, sep=" : ")
在
com.winlsr
包下创建
teacher
模块并定义
Teacher
类,它也是
Person
类的派生类:
from .person import Personclass Teacher(Person):# 无构造函数,创建对象时会调用父类构造函数def print_info(self):print(self.get_name(), self.get_id_number(), sep=" : ")
重启解释器,执行如下语句:
>>> from com.winlsr.actor import Actor>>> actor = Actor("xuzheng", 123456789)>>> actor.add_movie("我不是药神")>>> actor.print_info()xuzheng : 123456789 : [\'我不是药神\']>>> from com.winlsr.teacher import Teacher>>> teacher = Teacher()# Teacher 类中没有定义 __init__(),这里会调用父类 Person 的构造并传入参数>>> teacher = Teacher("lsl", 123459876)>>> teacher.print_info()lsl : 123459876
对应
Python Console
如下:
Python
也支持一种多重继承。 带有多个基类的类定义语句如下所示:
class DerivedClassName(Base1, Base2, Base3):<statement-1>...<statement-n>
派生类实例如果某一属性在
DerivedClassName
中未找到,则会到
Base1
中搜索它,然后(递归地)到
Base1
的基类中搜索,如果在那里未找到,再到
Base2
中搜索,依此类推。
其他
推荐学习官方文档的迭代器,生成器,生成器表达式。
结语
教程到这里就结束了,最后推荐大家再去看看廖雪峰老师讲解的
异常处理
和
IO
的内容(也可以用到的时候再看),他比官网讲解的更有条理。学完这些内容就基本入门了,今后可以根据自己应用的领域再进一步学习即可,比如深度学习、web开发等。