Python学习

引言

为什么学习Python?

换到新公司,遇到几次excel数据清洗的问题,使用java也可以做,但是比较麻烦。再加上这家公司技术栈是混合的,也有python承担一些重要的工作。所以,下决定好好学习下Python。

怎么学?

这里推荐github上一个项目:python100,我基本上就是跟着这个项目和官网教程学习的,入门非常合适。

ok,言归正传,我们开始Python之旅。

环境:

Python 3.9.6 (default, Aug 5 2022, 15:21:02)

[Clang 14.0.0 (clang-1400.0.29.102)] on darwin

语言元素

1. 变量类型

  • 整型:
    Python 2.x中有int和long两种类型的整数,但这种区分对Python来说意义不大,因此在Python 3.x中整数只有int这一种了,支持二进制(0b100 = 4)、八进制(0o100 = 64)、十进制(100)和十六进制(0x100 = 256)

  • 浮点型:
    也就是我们常说的小数,支持数学写法(11.2)和科学记数法(11.123e2)

  • 字符串类型:
    以单引号、双引号和三引号包括起来的文本(‘字符串1’ or “字符串2” or ‘’’可以换行的字符串’’’)

  • 布尔型:
    布尔值只有True、False两种,注意开头大写

  • 复数型:
    类型3+5j,和数学上复数表示一致,不常用了解即可

2. 变量命名

总所周知,命名是一个大问题,变量命名需要遵循以下这些必须遵守硬性规则和强烈建议遵守的非硬性规则。

  • 硬性规则:

    • 变量名由字母(广义的Unicode字符,不包括特殊字符)、数字和下划线构成,数字不能开头
    • 大小写敏感(大写的a和小写的A是两个不同的变量)
    • 不要跟关键字(有特殊含义的单词,后面会讲到)和系统保留字(如函数、模块等的名字)冲突
  • PEP 8要求:

    • 用小写字母拼写,多个单词用下划线连接。
    • 受保护的实例属性用单个下划线开头(后面会讲到)
    • 私有的实例属性用两个下划线开头(后面会讲到)

分支结构

我们先来看一个简单的demo

"""
简单登录

"""
name = str(input('请输入用户名:'))
passwprd = str(input('请输入密码:'))
if name != 'zz':
     print('用户名不存在')
elif name == 'zz' and passwprd == '123456': 
     print('验证成功😁')   
else:
     print("密码错误💔") 

注意⚠️:

input()是python的内置函数,作用是从命令行捕获输入的值.

str()是python的内置函数,作用将值变成字符串

我们可以看到分支结构主要作用是根据条件进入到不同的分支,执行不同的代码。这里需要注意的是,if后面空一个空格才能输入条件,而不是像java一样用括号包裹起来。而且,if下面的print()函数也需要一个在前面至少一个空格才能运行。

熟悉Java的朋友应该了解,分支结构除了if-elif-else这样的语法之外,还有一个switch语法。Python在3.10版本之后正式支持,语法结构如下:

def http_error(status):
    match status:
        case 400:
            return "Bad request"
        case 404:
            return "Not found"
        case 418:
            return "I'm a teapot"
        case _:
            return "Something's wrong with the internet"

循环结构

  • for 语句

    '''
    基础使用
    '''
    words = ['1','2','3']
    for w in words:
      print(w)
    
    ##print result#
    1
    2
    3  
    

    其实还有个for-else语法,其作用是没有遇到任何break的时候会跳到else分支。但是,比较反直觉,一般生产不推荐使用。

  • range() 函数

    '''
    range函数包含三个参数:
    start 一开始的值,默认为0
    stop 结束的值
    step 步长,每次数值递增的值
    计算范围是[start, stop)
    '''
    sum = 0
    #计算1到2之间,每次累积增加1范围内的值
    for x in range(1,3,1):
        sum += x
    print(sum)
    ##print result#
    3
    
  • while循环

    count = 0
    while True:
        count += 1
        print( count)
        if count>1:
            break
    ##print result#
    1
    2
    

    上面的代码中使用了break关键字来提前终止循环,需要注意的是break只能终止它所在的那个循环,这一点在使用嵌套的循环结构(下面会讲到)需要引起注意。除了break之外,还有另一个关键字是continue,它可以用来放弃本次循环后续的代码直接让循环进入下一轮。

函数

默认入参值

在Python中,函数参数可以有默认值,支持使用可变参数,所以不需要像Java一样支持函数的重载,举个例子:

def calculate(a=0, b=0):
    '''
    求和
    函数注释一般写在第一行下面,可以生成文档,
    VsCode中鼠标放到方法名上,可以预览注释内容
    '''
    return a +b

print(calculate())//0
print(calculate(1))//1
print(calculate(1,2))//3
print(calculate(b=2,a=1))//3

Keyword Arguments和Positional Arguments

这两个定义实在是不知道如何准确的翻译,我就把英语原文搬上来,具体定义如下:

  • Keyword Arguments: 函数入参中使用形如param_name = param_value的值参数,在定义参数的时候给定默认值就叫做Keyword Arguments
  • Positional Arguments: 函数入参中只有一个参数名称,没有给定默认值的叫做Positional Arguments

那么,这两种有什么特点呢?看下面Demo:

def test(a,b='b',c='c',d='d'):
    print(a)
    print(b)
    print(c)
    print(d)


test() #required argument missing
test(b=3, 1) #non-keyword argument after a keyword argument
test(110, a=220) #duplicate value for the same argument
test(zzz=1231) #unknown keyword argument

可以看出,Positional Arguments的参数特点:

  • 没有初始化,所以必须填值
  • 顺序很重要,如果顺序不一样,结果也是不一样
  • Positional Arguments必须要在Keyword Arguments参数前面

而Keyword Arguments的参数特点:

  • 一开始有默认值,所以不一定需要初始化
  • 因为有参数名指定参数值,所以顺序不重要
  • 不能传入没有定义的参数
  • 必须在Positional Arguments后面

特殊参数

一般情况下,Positional Arguments和Keyword Arguments并存,为了代码可读性考虑,增加特殊参数区分两者,例子:

def func(pos1, pos2, /, pos_or_kwd, *, kwd1=11, kwd2=22):
    print(pos1,pos2,pos_or_kwd, kwd1, kwd2)

func(1,2,1) #1 2 1 11 22

如上所示,/前代表Positional Arguments Only, *之后代表Keyword Arguments Only,中间部分代表Positional Or Keyword Arguments

来自官网的最佳实践:

  • 当希望参数对用户不可用的时候使用positional-only,即参数名称没有意义的时候
  • 对于api来说,使用positional-only可以防止参数名称改变了,对api的破坏性改动
  • 只有当参数有意义,更便于理解,或者你不想用户依赖参数位置的时候才使用keyword-only

任意参数列表

def arbitrary_param_fun(*arguments):
    print(arguments)
arbitrary_param_fun(1, 2, 3, 4, 5)  # (1, 2, 3, 4, 5)

def arbitrary_param_fun2(*arguments, name="s"):
    print(arguments, name)
arbitrary_param_fun2(1, 2, 3, 4, 5)  # (1, 2, 3, 4, 5) s

类似Java中的可变参数,只不过这里是以*开头,表示是一个任意的参数列表。

注意,当任意的参数列表在前面的时候,后面的参数只能是keyword-only形式

拆包参数列表

print(list(range(3, 6)) ) #[3, 4, 5]

args = [3,6]
print(list(range(*args)) ) #[3, 4, 5]

在参数已经是定义好的元组(tuple)或者列表中,可以以*开头,后面跟着参数变量,当做参数传入

Lambda表达式

lambda表达式就是一个匿名函数,具体如下:

def lambda_expression_func(n):
    '''
    返回一个匿名函数
    '''
    return lambda a: a+n

#f命名这个匿名函数,相当于 
# def f(a):
#   return a+1
f = lambda_expression_func(1);
print(f(0)) #1
print(f(1)) #2

函数注解

是一种元数据,与用户定义的类型信息相关。
注解存储在_annotations_属性中,作为字典,对其他部分没有影响

def f(a:string,v:string,c:int)->int:
    print("Annotation:", f.__annotations__)
    print(1111)
f(1,2,3) 
# Annotation: {'a': <module 'string' from '/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/string.py'>, 'v': <module 'string' from '/Library/Developer/CommandLineTools/Library/Frameworks/Python3.framework/Versions/3.9/lib/python3.9/string.py'>, 'c': <class 'int'>, 'return': <class 'int'>}
# 1111

数据结构

List详解

  • list.append(x):在列表尾部添加一个元素,和a[len(a):] = x等价
  • list.extend(iterable):传入一个可以迭代的结构,列表或者字符串,然后附加到列表尾部
  • list.insert(i, x):在i这个位置插入x,其他数据后移。所以,a.insert(0,x)在列表最前面添加,a.insert(len(a),x)等价于list.append(x)
  • list.remove(x):从列表中删除其值等于x的第一项。如果没有这样的项,则会引发ValueError。
  • list.pop([i]):移除列表指定位置i的元素,如果i没有指定移除最后列表尾部的元素
  • list.clear():移除所有元素
  • list.index(x[, start[, end]]):查找x所在位置,返回其下标记录(0开始),start和end是可选项,指定查找x的范围
  • list.count(x):返回x在list中出现的次数
  • list.sort(*, key=None, reverse=False):*上面函数的时候讲了代表分割符号,后面都是keyword arguments;key代表用于在元素中提取比较键的函数,默认值是None,代表直接比较元素;reverse是一个boolean值,如果设置为true,代表每个比较被反转(意思是,如果默认是升序,那么revese=True之后就是降序)
  • list.reverse():反转list的元素
  • list.copy():返回列表的浅拷贝
fruits = ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana']

fruits.append("andOne")
print(fruits)


fruits.extend('123')
ttt = ['aa','bb','cc']
fruits.extend(ttt)
print(fruits)

fruits.insert(0,"-1")
print(fruits)

fruits.pop(0)
print(fruits)
fruits.pop()
print(fruits)

index = fruits.index('apple')
print(index)

count_banana = fruits.count('banana')
print(count_banana)

fruits.sort()
print(fruits)
fruits.sort(reverse=True)
print(fruits)

fruits.reverse()
print(fruits)

copy_fruit = fruits.copy()
print(copy_fruit)


# ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana', 'andOne']
# ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana', 'andOne', '1', '2', '3', 'aa', 'bb', 'cc']
# ['-1', 'orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana', 'andOne', '1', '2', '3', 'aa', 'bb', 'cc']
# ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana', 'andOne', '1', '2', '3', 'aa', 'bb', 'cc']
# ['orange', 'apple', 'pear', 'banana', 'kiwi', 'apple', 'banana', 'andOne', '1', '2', '3', 'aa', 'bb']
# 1
# 2
# ['1', '2', '3', 'aa', 'andOne', 'apple', 'apple', 'banana', 'banana', 'bb', 'kiwi', 'orange', 'pear']
# ['pear', 'orange', 'kiwi', 'bb', 'banana', 'banana', 'apple', 'apple', 'andOne', 'aa', '3', '2', '1']
# ['1', '2', '3', 'aa', 'andOne', 'apple', 'apple', 'banana', 'banana', 'bb', 'kiwi', 'orange', 'pear']
# ['1', '2', '3', 'aa', 'andOne', 'apple', 'apple', 'banana', 'banana', 'bb', 'kiwi', 'orange', 'pear']

列表实现栈

栈的特点就是Last In,First Out,使用list可以很容易就能实现,具体可以看下面:

stack = [3, 4, 5]
stack.append(6)
stack.append(7)
stack.append(8)
print(stack)
print(stack.pop())
print(stack.pop())
print(stack.pop())
print(stack)

# [3, 4, 5, 6, 7, 8]
# 8
# 7
# 6
# [3, 4, 5]

列表实现队列

队列的特点就是First In,First Out,可以通过append和pop实现。但是,不够高效。
python中collections提供了高效的实现,具体如下:

from collections import deque

queue = deque(['a','b','c'])
queue.append('d')
print(queue) #deque(['a', 'b', 'c', 'd'])
poped_item = queue.popleft()
print(poped_item) #a

del语句

del可以用于删除列表中的一个元素或者整个列表

a = [1,2,3]
del a[0] #删除一个元素
print(a) #[2,3]
del a[:] #删除整个列表
print(a) #[]
del a #删除变量
print(a) #NameError: name 'a' is not defined

元组(Tuples)和序列(Sequences)

元组是一种标准的序列化类型,由逗号分隔开的许多值组成,比如:

t  = 111, 222, '123'
print(t[0])
print(t)
# t[0]=999  'tuple' object does not support item assignment

u = t, (1,2,3)
print(u)

v = ([1, 2, 3], [3, 2, 1])
v[0].append(7)
print(v)

empty = ()
singleton = 1,
print(empty) #()
print(singleton) #(1,)

a, b, c = (1,2,3)
print(f'a={a}, b={b}, c={c}')

元组和列表比较像,但是两者用法不同。元组是不可变的,列表可变,使用下标修改元组数据会报错。但是,元组中元素如果是可变的列表,那么就可以修改。
需要注意的是,空元组和单元素元组写法比较特殊。

元组,可以解构赋值。什么意思呢?就是两边元组一致的话,右边的元组元组可以赋值给左边的变量。这点,我觉得才是元组最大的意义,因为可以实现函数的多返回值。具体,如下:

def return_3_result():
    #do something
    return 1,2,3

a,b,c = return_3_result()
print(f'多返回值:a={a}, b={b}, c={c}')
#多返回值:a=1, b=2, c=3

Sets

集合是没有重复元素的无序集合,基本用途包括成员资格测试(判断一个元素是否在集合内)和消除重复项。集合对象还支持数学运算,如并集、交集、差分和对称差分。

使用{'1','2'}set()创建集合

注意:如果要创建空的集合必须使用set(),因为{}表示一个空的字典

basket1 = {'apple', 'orange', 'apple', 'pear', 'orange', 'banana'}
print(type(basket1)) #<class 'set'>
print('orange' in basket1) #True
print('orangeeee' in basket1) #False
basket2 = set('asda')
print(type(basket2)) #<class 'set'>
basket3 = set() 
print(type(basket3)) #<class 'set'>
basket4 = {}
print(type(basket4)) #<class 'dict'>

a = set('abracadabra')
b = set('alacazam')
print(a) #{'r', 'a', 'c', 'b', 'd'}
print(a - b) #{'d', 'b', 'r'}
print(a | b) #{'c', 'a', 'l', 'm', 'd', 'b', 'r', 'z'}
print(a & b) #{'c', 'a'}
print(a ^ b) #{'d', 'l', 'm', 'b', 'r', 'z'}

字典

所谓字典,我们可以理解成为多个键值对组成的数据结构。以下是简单的案例:

tel  = {'jack':1231, 'sape':2345}
print(tel) #{'jack': 1231, 'sape': 2345}
print(tel['jack']) #1231
del tel['sape']
print(tel) #{'jack': 1231}
tel['irv'] = 6666
print(tel) #{'jack': 1231, 'irv': 6666}
tel['guido'] =888
print(list(tel)) #['jack', 'irv', 'guido']
print(sorted(tel)) #['guido', 'irv', 'jack']
print('guido' in tel) #True
print('jack' not in tel)#False


##构造dict
d = dict([('sape', 4139), ('guido', 4127), ('jack', 4098)])
print(d) #{'sape': 4139, 'guido': 4127, 'jack': 4098}
dd = {x: x**2 for x in (2, 4, 6)}
print(dd) #{2: 4, 4: 16, 6: 36}
ddd = dict(sape=4139, guido=4127, jack=4098)
print(ddd) #{'sape': 4139, 'guido': 4127, 'jack': 4098}

模块

在Python中一个以.py结尾的文件,被称作一个模块(module),模块可以被其他模块引用。比如说,我创建一个文件叫a.py,这个文件就是一个模块,其中a就是模块名,可以通过__name__内置的函数查看。

模块包含可执行语句和函数,可执行语句只有在import第一次遇到模块名的时候才会执行(实际上,函数的定义也是可执行的,它执行的行为就是在全局的符号表中录入函数名)。

每个模块都有自己的私有符号表,这个符号表被这个模块的所有函数使用。因此,模块作者可以使用全局变量而不用担心变量冲突,在模块外部还可以通过modname.itemname的方式来引用。
模块之间的引用,可以通过下面的方式来实现:

from test import t1, t2
from test import * #不推荐,可读性不好
import t1 as tt1
from test import t1 as tt1

模块当作脚本执行

当我们需要把一个单独模块当成脚本执行的时候,我们需要创建一个文件,比如hello.py,然后写下如下代码:

def sum(a):
    return a

if __name__ == '__main__':
    import sys
    print(sum(sys.argv[1]))
    print(sum(sys.argv[2]))

接着,执行指令:python hello.py 10 20 30,会看到他会立刻执行里面的函数功能。

模块的搜索路径

当我们使用import关键字引入一个模块,如何才能找到这个模块的文件位置呢?

解释器首先会在它的内置模块去找(所有的模块名称,可以通过sys.builtin_module_names查找),如果没有找到会去找sys.path里面查找,sys.path包含以下位置:

  • 输入脚本的目录,如果没有默认当前目录(python test10.py 10 20 30)
  • PYTHONPATH
  • 依赖于安装的默认值(约定包括site-packages目录,由site模块处理)。

注意:在支持符号链接的系统中,在符号链接之后计算包含输入的脚本的目录。也就是说,包含符号链接的目录不会被添加到模块搜索路径中。

标准库

所谓标准库,就是一些非核心代码内置到python解释器中。主要的目的是为了提高效率,或者提供对系统操作的能力。

包是组织python模块的一种方式,简单来讲包就是相同功能模块的的集合,以下是个简单示例:

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

当导入包的时候,python在sys.path上搜索。

注意:

__init__.py在包中必须存在,这样才能将目录视为包,否则就是一个普通的目录,__init__.py内容可以是空,也可以是执行包初始化代码或者__all__变量初始化的代码。

__all__变量内容包括如下形式,__all__ = ["echo", "surround", "reverse"],主要目的是为了在使用import *时,告诉解释器要默认导入哪些模块。

输入和输出

更漂亮的格式化输出

通常,格式化有以下3种方式:

  • 使用格式化字符串,在引号或者三引号以字母f或者F开头,在引里面可以写一个python表达式并以{}包裹
  • 使用str.format(),仍使用{}包裹,要替换的位置,并提供详细的格式化命令
  • 使用str的切片功能自定义格式化

格式化字符串

格式化字符串也叫f-string,允许您在字符串前加上f或f并将表达式写成{expression},从而在字符串中包含Python表达式的值,举个例子:

import math
print(f"The value of pi is approximately {math.pi:.3f}.") #:.3f表示小数点后面三个字符宽度
print(F"The value of pi is approximately {math.pi:.3f}.")

#运行结果
#The value of pi is approximately 3.142.
#The value of pi is approximately 3.142.

字符串format()方法

使用字符串自带的format()格式化字符串,具体如下:

print('We are the {} who say "{}!"'.format('knights', 'Ni'))

print('{0} and {1}'.format('spam', 'eggs')) #使用参数的位置替换
print('{1} and {0}'.format('spam', 'eggs'))


print('This {food} is {adjective}.'.format(
      food='spam', adjective='absolutely horrible')) #使用keyword param替换
print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred',
                                                   other='Georg')) #使用位置和keyword paaram混合

table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; '
      'Dcab: {0[Dcab]:d}'.format(table))                                                   

#运行结果
#We are the knights who say "Ni!"
#spam and eggs
#eggs and spam
#This spam is absolutely horrible.
#The story of Bill, Manfred, and Georg.
#Jack: 4098; Sjoerd: 4127; Dcab: 8637678

手动格式化字符串

直接使用print()一个一个拼接,最原始的方式,不推荐

旧的字符串格式化方式

import math
print('The value of pi is approximately %5.3f.' % math.pi)

#运行结果
#The value of pi is approximately 3.142.

读写文件

系统内置函数open()能够返回一个文件对象,最常用的有三个参数,open(filename, mode, encoding=None),下面我来解释一下:

  1. filename:文件的完整路径,比如我演示程序中的/Users/knight/PythonProjects/test1/demo1.py
  2. mode:设置文件的读写模式,r代表只读是默认选项,w代表只写,a代表追加到文件尾,**r+**代表读写
  3. encoding:编码格式,例如’utf-8’等

python推荐文件读写最佳实践是以with开头的方式,下面看例子:

with open("/Users/knight/PythonProjects/test1/demo1.py", mode = "r+", encoding="utf-8") as f:
    read_data = f.read()
    print(read_data)

#运行结果
#print("测试文件")

这种方式可以不用考虑文件开启和关闭。否则,需要使用try-finally语句调用f.close()去手动关闭文件流。

文件对象常用方法

f = open("/Users/knight/PythonProjects/test1/demo1.py", mode = "r+", encoding="utf-8")
f.read() #读取所有文件内容
f.readLine() #读取文件一行内容

for line in f:
    print(line, end='') #最佳实践读取所有文件内容

f.write('This is a test\n') #文件尾部写入,返回写入数据长度,只能写字符串或者二进制,其他数据类型需要转换

使用json保存结构化对象

import json

f = open("/Users/knight/PythonProjects/test1/demo1.py", mode = "r+", encoding="utf-8")
x = [1, 'zjhu','ada']
json.dump(x, f)

ff= open("/Users/knight/PythonProjects/test1/demo1.py", mode = "r+", encoding="utf-8")
x = json.load(ff)
print(x)

#运行结果
#[1, 'zjhu', 'ada']

和其他语言一样,类提供一种将数据和功能绑定在一起的方式。

名称和对象

对象具有独立性,多个名称可以绑定到一个对象上,在其他语言中叫做别名。

在处理不可变的基本类型(数字、字符串、元组)的时候,可以忽略。然而,涉及到可变对象的时候(列表、字典和大部分类型)的时候,会有令人惊讶的影响。这种影响对于程序是有益的,别名在某些方面类似指针(类似Java中基本类型和对象类型的引用)

Python的作用域(scope)和命名空间(namespace)

命名空间(Namespace)是从名称到对象的映射,大部分的命名空间都是通过 Python 字典来实现的。

命名空间提供了在项目中避免名字冲突的一种方法。各个命名空间是独立的,没有任何关系的,所以一个命名空间中不能有重名,但不同的命名空间是可以重名而没有任何影响。

命名空间的分类

命名空间分为三种:

  • 内置名称(build-in names),在python解释器开始运行时,就会申请生成的地方,在这里存放的都是系统内置的名称,比如python关键字,print,import等等,或者内置的变量pi;该namespace只有一个,只有当python解释器退出的时候,才会进行释放回收。
  • 全局名称(global names),在程序代码解释过程中,只要不是函数体内创建的name,都存放在全局namespace中,包含变量名、函数名、类名等等。作用域整个python程序,当该python程序运行完成后,才会进行清空回收。
  • 局部名称(lcoal names),在函数体内定义的name,作用域只在函数体内,调用一次函数即会产生一次namespace,每个函数实例都会有自己的namespace,当函数调用完成后,就会释放消失

命名空间的顺序

生成顺序:1.内置namespace,2.全局namespace,3.局部namespace

销毁顺序:1.局部namespace,2.全局namespace,3.内置namespace

查找顺序:当前位置,向上查找,如果在全局,只能差全局和内置,如果在局部,三个namespace都可以查找

命名空间的生命周期

命名空间的生命周期取决于对象的作用域,如果对象执行完成,则该命名空间的生命周期就结束

作用域(Scope)

作用域就是一个 Python 程序可以直接访问命名空间的正文区域

有四种作用域:

  1. L(Local):最内层,包含局部变量,比如一个函数/方法内部
  2. E(Enclosing):包含了非局部(non-local)也非全局(non-global)的变量。比如两个嵌套函数,一个函数(或类) A 里面又包含了一个函数 B ,那么对于 B 中的名称来说 A 中的作用域就为 nonlocal
  3. G(Global):当前脚本的最外层,比如当前模块的全局变量
  4. B(Built-in): 包含了内建的变量/关键字等,最后被搜索

顺序:L->E->G->B,在局部找不到,便会去局部外的局部找(例如闭包),再找不到就会去全局找,再者去内置中找。

全局变量和局部变量

total = 0 # 这是一个全局变量
# 可写函数说明
def sum( arg1, arg2 ):
    #返回2个参数的和."
    total = arg1 + arg2 # total在这里是局部变量.
    print ("函数内是局部变量 : ", total)
    return total
 
#调用sum函数
sum( 10, 20 )
print ("函数外是全局变量 : ", total)

#输出结果:
#函数内是局部变量 :  30
#函数外是全局变量 :  0

global 和 nonlocal关键字

global用于修改全局变量

num = 1
def fun1():
    global num  # 需要使用 global 关键字声明
    print(num) #打印1 
    num = 123
    print(num) #打印123
fun1()
print(num) #打印123

nonlocal用于修改嵌套作用域

num = 0 #全局作用域
def outer():
    num = 10 #outer的局部作用域
    def inner():
        nonlocal num   # nonlocal关键字声明,修改outer的局部作用域num
        num = 100
        print(num) #100
    inner()
    print(num) #100
outer()
print(num) #没有修改所以还是0

定义类的语法

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

类对象

类对象支持两种操作,属性引用实例化

属性引用通过标准化的.来直接引用,类对象创建之后所有的命名都是有效的属性名,如果类定义是这样的:

class DemoClass:
    '''类的简单例子'''
    i = 12345
    def f(self):
        return "hello class in py"

#实例化DemoClass     
x = DemoClass()
#访问类的属性和方法
print(x.i) #12345
print(x.f()) #hello class in py

有时候,在实例化类的时候,我们希望初始化自己的状态,因此python提供了一个构造器__init__()

class ComplexClass:
    '''稍微复杂的例子'''
    #定义构造方法
    def __init__(self, realpart:int, imagpart:str) -> None:
        self.r = realpart
        self.i = imagpart

x = ComplexClass(1,'333')
print(x.i) #333
print(x.r) #1

其中self代表的是类的实例化对象,指向对象的地址,而self.__class__才是指向类的。

注意⚠️:

self不是关键字,可以用其他符号代替,比如我换成cc,结果也是一样的

在类的内部,使用 def 关键字来定义一个方法,与一般函数定义不同,类方法必须包含参数 self, 且为第一个参数,self 代表的是类的实例

#类定义
class people:
    #定义基本属性
    name = ''
    age = 0
    #定义私有属性,私有属性在类外部无法直接进行访问
    __weight = 0
    #定义构造方法
    def __init__(self,n,a,w):
        self.name = n
        self.age = a
        self.__weight = w
    def speak(self):
        print("%s 说: 我 %d 岁。" %(self.name,self.age))
 
# 实例化类
p = people('runoob',10,30)
p.speak()

继承

语法如下:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

子类(派生类 DerivedClassName)会继承父类(基类 BaseClassName)的属性和方法。

BaseClassName(实例中的基类名)必须与派生类定义在一个作用域内。除了类,还可以用表达式,基类定义在另一个模块中时这一点非常有用:

class DerivedClassName(modname.BaseClassName):

简单案例:

#类定义
class people:
    #定义基本属性
    name = ''
    age = 0
    #定义私有属性,私有属性在类外部无法直接进行访问
    __weight = 0
    #定义构造方法
    def __init__(self,n,a,w):
        self.name = n
        self.age = a
        self.__weight = w
    def speak(self):
        print("%s 说: 我 %d 岁。" %(self.name,self.age))
 
#单继承示例
class student(people):
    grade = ''
    def __init__(self,n,a,w,g):
        #调用父类的构函
        people.__init__(self,n,a,w)
        self.grade = g
    #覆写父类的方法
    def speak(self):
        print("%s 说: 我 %d 岁了,我在读 %d 年级"%(self.name,self.age,self.grade))
 
 
 
s = student('ken',10,60,3)
s.speak() #ken 说: 我 10 岁了,我在读 3 年级

多继承

py支持有限的多继承,语法如下:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

需要注意圆括号中父类的顺序,若是父类中有相同的方法名,而在子类使用时未指定,python从左至右搜索 即方法在子类中未找到时,从左到右查找父类中是否包含方法。

#类定义
class people:
    #定义基本属性
    name = ''
    age = 0
    #定义私有属性,私有属性在类外部无法直接进行访问
    __weight = 0
    #定义构造方法
    def __init__(self,n,a,w):
        self.name = n
        self.age = a
        self.__weight = w
    def speak(self):
        print("%s 说: 我 %d 岁。" %(self.name,self.age))
 
#单继承示例
class student(people):
    grade = ''
    def __init__(self,n,a,w,g):
        #调用父类的构函
        people.__init__(self,n,a,w)
        self.grade = g
    #覆写父类的方法
    def speak(self):
        print("%s 说: 我 %d 岁了,我在读 %d 年级"%(self.name,self.age,self.grade))
 
#另一个类,多重继承之前的准备
class speaker():
    topic = ''
    name = ''
    def __init__(self,n,t):
        self.name = n
        self.topic = t
    def speak(self):
        print("我叫 %s,我是一个演说家,我演讲的主题是 %s"%(self.name,self.topic))
 
#多重继承
class sample(speaker,student):
    a =''
    def __init__(self,n,a,w,g,t):
        student.__init__(self,n,a,w,g)
        speaker.__init__(self,n,t)
 
test = sample("Tim",25,80,4,"Python")
#方法名同,默认调用的是在括号中参数位置排前父类的方法
test.speak()   #我叫 Tim,我是一个演说家,我演讲的主题是 Python

方法重写

如果你的父类方法的功能不能满足你的需求,你可以在子类重写你父类的方法,实例如下:

class Parent:        # 定义父类
   def myMethod(self):
      print ('调用父类方法')
 
class Child(Parent): # 定义子类
   def myMethod(self):
      print ('调用子类方法')
 
c = Child()          # 子类实例
c.myMethod()         # 子类调用重写方法
#用子类对象调用父类已被覆盖的方法
super(Child,c).myMethod() #super() 函数是用于调用父类(超类)的一个方法。

#打印结果
#调用子类方法
#调用父类方法

类属性和方法

  • 类私有属性:

    __private_attrs:两个下划线开头,声明该属性为私有,不能在类的外部被使用或直接访问。在类内部的方法中使用时 self.__private_attrs

  • 类方法

    在类的内部,使用 def 关键字来定义一个方法,与一般函数定义不同,类方法必须包含参数 self,且为第一个参数,self 代表的是类的实例。

    self 的名字并不是规定死的,也可以使用 this,但是最好还是按照约定使用 self

  • 类的私有方法

    __private_method:两个下划线开头,声明该方法为私有方法,只能在类的内部调用 ,不能在类的外部调用。self.__private_methods

案例:

class JustCounter:
    __secretCount = 0  # 私有变量
    publicCount = 0    # 公开变量
 
    def count(self):
        self.__secretCount += 1
        self.publicCount += 1
        print (self.__secretCount)
 
counter = JustCounter()
counter.count()
counter.count()
print (counter.publicCount)
print (counter.__secretCount)  # 报错,实例不能访问私有变量

#执行结果
1
2
2
Traceback (most recent call last):
  File "test.py", line 16, in <module>
    print (counter.__secretCount)  # 报错,实例不能访问私有变量
AttributeError: 'JustCounter' object has no attribute '__secretCount'

类专有的方法

  • init : 构造函数,在生成对象时调用
  • del : 析构函数,释放对象时使用
  • repr : 打印,转换
  • setitem : 按照索引赋值
  • getitem: 按照索引获取值
  • len: 获得长度
  • cmp: 比较运算
  • call: 函数调用
  • add: 加运算
  • sub: 减运算
  • mul: 乘运算
  • truediv: 除运算
  • mod: 求余运算
  • pow: 乘方

同样的,这些方法也支持重载

错误和异常

语法错误

Python 的语法错误或者称之为解析错,是初学者经常碰到的,如下实例:

while True print('Hello world')
  File "<stdin>", line 1, in ?
    while True print('Hello world')
    
SyntaxError: invalid syntax    

异常

即使语法正常,运行它的时候,也有可能发生错误,运行期检测到的错误被称为异常。

大多数的异常都不会被程序处理,都以错误信息的形式展现在这里:

>>> 10 * (1/0)             # 0 不能作为除数,触发异常
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
ZeroDivisionError: division by zero
>>> 4 + spam*3             # spam 未定义,触发异常
Traceback (most recent call last):
  File "<stdin>", line 1, in ?
NameError: name 'spam' is not defined
>>> '2' + 2               # int 不能与 str 相加,触发异常
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: can only concatenate str (not "int") to str

异常以不同的类型出现,这些类型都作为信息的一部分打印出来: 例子中的类型有 ZeroDivisionError,NameError 和 TypeError。

错误信息的前面部分显示了异常发生的上下文,并以调用栈的形式显示具体信息。

异常处理

语法如下:

try:
    #执行可能有异常代码
    ---
except AssertionError as error:
    #发生异常的时候执行的代码
    ---
else:
      #没有异常的时候执行的代码
    ---
finally:
      #无论如何都会执行的代码
    print('这句话,无论异常是否发生都会执行。')

抛出异常

Python 使用 raise 语句抛出一个指定的异常,语法如下

#语法
raise [Exception [, args [, traceback]]]

#实现
x = 10
if x > 5:
    raise Exception('x 不能大于 5。x 的值为: {}'.format(x))
    
#结果
Traceback (most recent call last):
  File "/Users/knight/PythonProjects/test1/demo11.py", line 3, in <module>
    raise Exception('x 不能大于 5。x 的值为: {}'.format(x))
Exception: x 不能大于 5。x 的值为: 10

用户自定义异常

你可以通过创建一个新的异常类来拥有自己的异常。异常类继承自 Exception 类,可以直接继承,或者间接继承,例如:

#自定义异常
class MyError(Exception):
        def __init__(self, value):
            self.value = value
        def __str__(self):
            return repr(self.value)

#使用自定义异常
try:
    raise MyError(2*2)
except MyError  as e:
    print(f"My exception occurred, value: {e}")
    
#结果
#My exception occurred, value: 4    

定义清理行为

try 语句还有另外一个可选的子句(finally),它定义了无论在任何情况下都会执行的清理行为。

预定义的清理行为

一些对象定义了标准的清理行为,无论系统是否成功的使用了它,一旦不需要它了,那么这个标准的清理行为就会执行。

比如我们前面讲的with语句,就可以保证诸如文件之类的对象在使用完之后一定会正确的执行他的清理方法:

with open("xxx.txt") as f:
    for line in f:
        print(line, end="")

结尾

自此,我们python的基础学习告一段落。

其实,学习的时候发现很多语法或者功能都比较熟悉。毕竟,优秀的设计大家都会互相借鉴。

ok,后面python的文章可能就偏向于python的标准库实战了。毕竟,这才是pyhton的精华所在。