python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片

Day 1: Python两大特征和四大基本语法

Python的两大基本特征:

Python 是一门动态的、强类型语言。

什么是动态语言?

要了解什么是动态语言,要首先了解“类型检查”。

类型检查是验证类型约束的过程,编译器或解释器通常在编译阶段或运行阶段做类型检查。

类型检查就是查看“变量”和它们的”类型”,然后判断表达式是否合理。例如,不能拿一个 string 类型变量除以浮点数变量。

如果类型检查发生在程序运行阶段(run time),那么它便是“动态类型语言”(dynamically typed languages)。常见的动态语言包括:

  • Python
  • JavaScrpit
  • PHP

类型检查发生在“编译阶段”(compile time)的是“静态类型语言”(statically typed languages)。常见的静态类型语言包括:

  • C
  • C++
  • Java
  • C#
  • Scala
什么是强类型语言?

强类型语言是指:不管是在编译阶段还是运行阶段,一旦某种类型绑定到变量后,此变量便会持有此类型,并且不能同其他类型在计算表达式时,混合使用。

例如,在交互式工具 IPython 中输入如下两行代码:

In [1]: a = 5
In [2]: a = a + 's'

程序会抛出 TypeError 异常:

unsupported operand type(s) for +: 'int' and 'str'

意思是不支持 int 变量和 str 变量相加。

常见的强类型语言有:

  • Python
  • Java
  • C#
  • Scala

与之对应的是弱类型语言,弱类型语言容易与其他类型混合计算。弱类型语言代表 JavaScript。

支持如下操作:

var data = 5
data = data + 'xiaoming' //string 和 int 结合自动转化为 string

常见的弱类型语言有:

  • C
  • C++
  • PHP
  • Javascript

如下,按照是否为静态/动态语言,弱类型/强类型两个维度,总结常用的语言分类。

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第1张图片

四大基本语法

四大基本语法:变量命名规则、缩进原则、特殊关键字和特殊运算符

命名规则

Python 的变量命名规则主要包括两条:

  • 允许包括英文、数字以及下划线(_),不能以数字开头
  • 名称区分大小写

特别说明以“下划线”开头的变量是有特殊意义的:

  • 类变量若以单下划线_)开头,代表不能直接被访问,类似于 C# 的受保护型变量(protected),表示不能通过 import module_name 而导入。
  • 类变量若以双下划线__)开头,表示为类的私有成员,不能被导入和其他类变量访问。
  • 以双下划开头和双下划线结尾的变量是 Python 里的专用标识,有特殊的身份。

如 Python 自定义类中都包括 initadd 方法,如果不重写 add 去执行两个类加法操作,程序会抛 TypeError 异常。只有重写后,程序才能正常执行加法操作。

Python 变量命名习惯一般遵守蛇形命名法(snake case):

  • 一般变量命名,book_id、book_store_count;
  • 类名首字符为大写,如 Python 内置模块 collections.abc 中的 Iterable 类、我们自定义的 Book 类等;
  • 类方法名:get_store_count();
  • 其他特殊变量,会全部大写,M_PI、MAX_VEHICLE_SPEED。

这与 Java 命名方法不同,Java 最典型的命名方法——驼峰命名法(camel case)。

缩进原则

Python 最具特色的地方就是用缩进代替 Java、C++ 中的 {},缩进的层级结构表示代码的逻辑层次。

比如,自定义一个 Book 类,重写 add 方法计算两类书的库存量和。

Python 的缩进方法,一般为 4 个字符。

  • 代码行 class Book(object) 与代码行 # 定义类的参数 的缩进,此处为 4 个字符;
  • 代码行 def __add__(self,book):return 所在行缩进也是 4 个字符。

通过这种层级结构,展现代码的逻辑层次。

下面代码,创建一个 Book 类:

class Book(object):
    # 定义类的参数
    def __init__(self,book_id, book_name, book_store_count):
        self.book_id = book_id
        self.book_name = book_name
        self.book_store_count = book_store_count
    # 重写加法操作    
    def __add__(self,book):
        return self.book_store_count + book.book_store_count

# 创建两个 Book 类的实例:
python_intro_book = Book(1,'python入门书',100) 
ml_intro_book = Book(2,'机器学习入门书',200)
# 求两本书的总销量
sales_cnt = python_intro_book + ml_intro_book
print(sales_cnt) # 300

如下是代码执行结果的演示图,打印总销量 300。结合图形,辅助大家快速理解代码。

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第2张图片

为了帮助新手更容易理解代码整个执行过程,专栏会配备相应的演示动画:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第3张图片

缩进格式、行间空行数、变量和等号空格等 Python 编码规范参考PEP8。

autopep8 包遵循 PEP8 的所有规范,安装此包,做好相关配置,便可自动实现 PEP8 制定的编码规范。

特殊关键字

Python 有 35 个关键字:

False      await      else       import     pass
None       break      except     in         raise
True       class      finally    is         return
and        continue   for        lambda     try
as         def        from       nonlocal   while
assert     del        global     not        with
async      elif       if         or         yield

自定义变量名不能与它们重复。

常用且不同于其他常用语言 C++ 和 Java 的关键字,如:

  • TrueFalse 用于表示值的真假,在 Java 中是 truefalse
  • 逻辑反操作 Python 使用 not,Java 是 !
  • None 表示空值,Java 使用 null
  • Python 两个条件同时满足使用 and,Java 是 &&
  • 两者满足其一,Python 使用 or,Java 使用 ||
  • Python 使用 elif, Java 是 else if

其他比较特殊的关键字,如:

  • del 用于删除可迭代对象中某个元素;
  • def 用于定义函数;
  • yield 用于定义生成器(generator)函数;
  • globalnonlocal 一种应用是 Python 函数式编程的闭包场景;
  • pass 一种应用是定义接口,也是 Python 语言特有的一个关键字。

这些关键字的用法,会在后续文章,更为详细的介绍。在此,先构建一个整体上的认识即可。

特殊运算符

Python 的运算符包括:

+       -       *       **      /       //      %      @
<<      >>      &       |       ^       ~       :=
<       >       <=      >=      ==      !=

大部分运算符应该被熟知,重点介绍 3 个比较特殊的://**:=

// 用于两个数值相除且向下取整,与 Python 的 math 模块中 floor 功能相似:

In [1]: 5//2
Out[1]: 2
In [2]: 5//4.5
Out[2]: 1.0

** 用于幂运算:

In [1]: 2**3
Out[1]: 8

:= 是在 2019 年,Python 3.8 版本里,刚刚才被支持的运算符,被形象地称为“海象运算符”。

n = len(a)
if n > 10:
    print(f"{n}大于10")

如果使用“海象运算符”,写法上更为精简:

if (n := len(a)) > 10:
    print(f"{n}大于10")

Python 比较运算符还支持链式比较,应用起来更加方便,比如:

i = 3
print(1 < i < 3) # False
print(1 < i <= 3) # True

另外,运算符 @ 用于装饰器功能,本专栏会深入解释它的本质,同时配备的几个相关案例,一定会帮助你学会使用装饰器。

小结

Python 学习第一天,首先认识 Python 两大特征:

  • 动态语言:动态指代码运行时才被编译器一行一行翻译执行;
  • 强类型:强类型指被绑定一个类型后便不能修改,不能与其他类型混用。

四大基本语法,总结了 Python 的命名规则、缩进原则、特殊关键字、特殊运算符,为后面的学习打下基础。

Day 2:Python 四大数据类型总结

基本数据类型

数值型

Python 中的数据皆是对象,比如被熟知的 int 整型对象、float 双精度浮点型、bool 逻辑对象,它们都是单个元素。举两个例子。

前缀加 0x,创建一个十六进制的整数:

0xa5 # 等于十进制的 165

使用 e 创建科学计数法表示的浮点数:

1.05e3 # 1050.0
容器型

可容纳多个元素的容器对象,常用的比如:list 列表对象、 tuple 元组对象、dict 字典对象、set 集合对象。Python 定义这些类型的变量,语法非常简洁。

举例如下。

使用一对中括号 [],创建一个 list 型变量:

lst = [1,3,5] # list 变量

示意图看出,右侧容器为开环的,意味着可以向容器中增加和删除元素:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第4张图片

使用一对括号 (),创建一个 tuple 型对象:

tup = (1,3,5) # tuple 变量

示意图看出,右侧容器为闭合的,意味着一旦创建元组后,便不能再向容器中增删元素:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第5张图片

但需要注意,含单个元素的元组后面必须保留一个逗号,才被解释为元组。

tup = (1,) # 必须保留逗号

否则会被认为元素本身:

In [14]: tup=(1)
    ...: print(type(tup)) 
<class 'int'>

使用一对花括号 {} 另使用冒号 :,创建一个 dict 对象:

dic = {'a':1, 'b':3, 'c':5} # dict变量

字典是一个哈希表,下面的示意图形象的表达出字典的 “形”。

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第6张图片

仅使用一对花括号 {},创建一个 set 对象:

s = {1,3,5} # 集合变量

Python 的容器类型,list、dict、tuple、set 等能方便地实现强大的功能,下面给出几个案例。

1. 去最求平均

去掉列表中的一个最小值和一个最大值后,计算剩余元素的平均值。

def score_mean(lst):
    lst.sort()
    lst2=lst[1:-1]
    return round((sum(lst2)/len(lst2)),1) # round四舍五入

lst=[9.1, 9.0,8.1, 9.7, 19,8.2, 8.6,9.8]
score_mean(lst) # 9.1

代码执行过程,动画演示:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第7张图片

2. 打印 99 乘法表

打印出如下格式的乘法表:

1*1=1
1*2=2   2*2=4
1*3=3   2*3=6   3*3=9
1*4=4   2*4=8   3*4=12  4*4=16
1*5=5   2*5=10  3*5=15  4*5=20  5*5=25
1*6=6   2*6=12  3*6=18  4*6=24  5*6=30  6*6=36
1*7=7   2*7=14  3*7=21  4*7=28  5*7=35  6*7=42  7*7=49
1*8=8   2*8=16  3*8=24  4*8=32  5*8=40  6*8=48  7*8=56  8*8=64
1*9=9   2*9=18  3*9=27  4*9=36  5*9=45  6*9=54  7*9=63  8*9=72  9*9=81

一共有 10 行,第 i 行的第 j 列等于:j*i,其中:

  • i 取值范围:1<=i<=9
  • j 取值范围:1<=j<=i

根据“例子分析”的语言描述,转化为如下代码:

In [13]: for i in range(1,10):
    ...:     for j in range(1,i+1):
    ...:         print('%d*%d=%d'%(j,i,j*i),end='\t')
    ...:     print()

3. 样本抽样

使用 sample 抽样,如下例子从 100 个样本中随机抽样 10 个。

from random import randint,sample
lst = [randint(0,50) for _ in range(100)] # 这里的下划线表示临时变量,相当于i,由于i用不到,所以补丁已具体的变量名称
print(lst[:5])# [38, 19, 11, 3, 6]
lst_sample = sample(lst,10)
print(lst_sample) # [33, 40, 35, 49, 24, 15, 48, 29, 37, 24]
字符串

注意 Python 中没有像 C++ 表示的字符类型(char),所有的字符或串都被统一为 str 对象。如单个字符 c 的类型也为 str。

str 类型会被经常使用,先列举 5 个被高频使用的方法。

strip 用于去除字符串前后的空格:

In [1]: '  I love python\t\n  '.strip()
Out[1]: 'I love python'

replace 用于字符串的替换:

In [2]: 'i love python'.replace(' ','_')
Out[2]: 'i_love_python'

join 用于合并字符串:

In [3]: '_'.join(['book', 'store','count'])
Out[3]: 'book_store_count'

title 用于单词的首字符大写:

In [4]: 'i love python'.title()
Out[4]: 'I Love Python'

find 用于返回匹配字符串的起始位置索引:

In [5]: 'i love python'.find('python')
Out[5]: 7

举个应用字符串的案例,判断 str1 是否由 str2 旋转而来。

字符串 stringbook 旋转后得到 bookstring,写一段代码验证 str1 是否为 str2 旋转得到。

转化为判断:str1 是否为 str2+str2 的子串。

下面函数原型中,注明了每个参数的类型、返回值的类型,增强代码的可读性和可维护性。

def is_rotation(s1: str, s2: str) -> bool:
    if s1 is None or s2 is None:
        return False
    if len(s1) != len(s2):
        return False

    def is_substring(s1: str, s2: str) -> bool:
        return s1 in s2
    return is_substring(s1, s2 + s2)

测试函数 is_rotation:

r = is_rotation('stringbook', 'bookstring')
print(r)  # True

r = is_rotation('greatman', 'maneatgr')
print(r)  # False

代码执行过程,动画演示:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第8张图片

字符串的匹配操作除了使用 str 封装的方法外,Python 的 re 正则模块功能更加强大,写法更为简便,广泛适用于爬虫、数据分析等。

下面这个案例实现:密码安全检查,使用正则表达式非常容易实现。

密码安全要求:

  • 要求密码为 6 到 20 位;
  • 密码只包含英文字母和数字。
import re
pat = re.compile(r'\w{6,20}') # 这是错误的,因为 \w 通配符匹配的是字母,数字和下划线,题目要求不能含有下划线
# 使用最稳的方法:\da-zA-Z 满足“密码只包含英文字母和数字”
# \d匹配数字 0-9
# a-z 匹配所有小写字符;A-Z 匹配所有大写字符
pat = re.compile(r'[\da-zA-Z]{6,20}')

选用最保险的 fullmatch 方法,查看是否整个字符串都匹配。

以下测试例子都返回 None,原因都在解释里。

pat.fullmatch('qaz12') # 返回 None,长度小于 6
pat.fullmatch('qaz12wsxedcrfvtgb67890942234343434') # None 长度大于 22
pat.fullmatch('qaz_231') # None 含有下划线

下面这个字符串 n0passw0Rd 完全符合:

In [20]: pat.fullmatch('n0passw0Rd')
Out[20]: <re.Match object; span=(0, 10), match='n0passw0Rd'>
自定义类型

Python 使用关键字 class 定制自己的类,self 表示类实例对象本身。

一个自定义类内包括属性、方法,其中有些方法是自带的。

类(对象):

class Dog(object):
    pass

以上定义一个 Dog 对象,它继承于根类 object,pass 表示没有自定义任何属性和方法。

下面创建一个 Dog 类型的实例:

wangwang = Dog()

Dog 类现在没有定义任何方法,但是刚才说了,它会有自带的方法,使用 dir() 查看这些自带方法:

In [26]: wangwang.__dir__()
Out[26]:
['__module__',
 '__dict__',
 '__weakref__',
 '__doc__',
 '__repr__',
 '__hash__',
 '__str__',
 '__getattribute__',
 '__setattr__',
 '__delattr__',
 '__lt__',
 '__le__',
 '__eq__',
 '__ne__',
 '__gt__',
 '__ge__',
 '__init__',
 '__new__',
 '__reduce_ex__',
 '__reduce__',
 '__subclasshook__',
 '__init_subclass__',
 '__format__',
 '__sizeof__',
 '__dir__',
 '__class__']

有些地方称以上方法为魔法方法,它们与创建类时自定义个性化行为有关。比如:

  • init 方法能定义一个带参数的类;
  • new 方法自定义实例化类的行为;
  • getattribute 方法自定义读取属性的行为;
  • setattr 自定义赋值与修改属性时的行为。

类的属性:

def __init__(self, name, dtype):
     self.name = name
     self.dtype = dtype

通过 init,定义 Dog 对象的两个属性:name、dtype。

类的实例:

wangwang = Dog('wangwang','cute_type')

wangwangDog 类的实例。

类的方法:

def shout(self):
    print('I\'m %s, type: %s' % (self.name, self.dtype))

注意:

  • 自定义方法的第一个参数必须是 self,它指向实例本身,如 Dog 类型的实例 dog;
  • 引用属性时,必须前面添加 self,比如 self.name 等。

总结以上代码:

In [40]: class Dog(object):
    ...:     def __init__(self,name,dtype):
    ...:         self.name=name
    ...:         self.dtype=dtype
    ...:     def shout(self):
    ...:         print('I\'m %s, type: %s' % (self.name, self.dtype))

In [41]: wangwang = Dog('wangwang','cute_type')

In [42]: wangwang.name
Out[42]: 'wangwang'

In [43]: wangwang.dtype
Out[43]: 'cute_type'

In [44]: wangwang.shout()
I'm wangwang, type: cute_type

看到创建的两个属性和一个方法都被暴露在外面,可被 wangwang 调用。这样的话,这些属性就会被任意修改:

In [49]: wangwang.name='wrong_name'

In [50]: wangwang.name
Out[50]: 'wrong_name'

如果想避免属性 name 被修改,可以将它变为私有变量。改动方法:属性前加 2 个 _ 后,变为私有属性。如:

In [51]: class Dog(object):
    ...:     def __init__(self,name,dtype):
    ...:         self.__name=name
    ...:         self.__dtype=dtype
    ...:     def shout(self):
    ...:         print('I\'m %s, type: %s' % (self.name, self.dtype))

同理,方法前加 2 个 _ 后,方法变为“私有方法”,只能在 Dog 类内被共享使用。

但是这样改动后,属性 name 不能被访问了,也就无法得知 wangwang 的名字叫啥。不过,这个问题有一种简单的解决方法,直接新定义一个方法就行:

def get_name(self):
    return self.__name

综合代码:

In [52]: class Dog(object):
    ...:     def __init__(self,name,dtype):
    ...:         self.__name=name
    ...:         self.__dtype=dtype
    ...:     def shout(self):
    ...:         print('I\'m %s, type: %s' % (self.name, self.dtype))
    ...:     def get_name(self):
    ...:         return self.__name
    ...:

In [53]: wangwang = Dog('wangwang','cute_type')

In [54]: wangwang.get_name()
Out[54]: 'wangwang'

但是,通过此机制,改变属性的可读性或可写性,怎么看都不太优雅!因为无形中增加一些冗余的方法,如 get_name。

下面,通过另一个例子,解释如何更优雅地改变某个属性为只读或只写。

自定义一个最精简的 Book 类,它继承于系统的根类 object:

class Book(object):
    def __init__(self,name,sale):
        self.__name = name
        self.__sale = sale

使用 Python 自带的 property 类,就会优雅地将 name 变为只读的。

    @property
    def name(self):
        return self.__name

使用 @property 装饰后 name 变为属性,意味着 .name 就会返回这本书的名字,而不是通过 .name() 这种函数调用的方法。这样变为真正的属性后,可读性更好。

In [101]: class Book(object):
     ...:     def __init__(self,name,sale):
     ...:         self.__name = name
     ...:         self.__sale = sale
     ...:     @property
     ...:     def name(self):
     ...:         return self.__name

In [102]: a_book = Book('magic_book',100000)

In [103]: a_book.name
Out[103]: 'magic_book'

property 是 Python 自带的类,前三个参数都是函数类型。更加详细的讨论放在后面讨论装饰器时再展开。

In [104]: help(property)
Help on class property in module builtins:

class property(object)
 |  property(fget=None, fset=None, fdel=None, doc=None)

如果使 name 既可读又可写,就再增加一个装饰器 @name.setter。

In [105]: class Book(object):
     ...:     def __init__(self,name,sale):
     ...:         self.__name = name
     ...:         self.__sale = sale
     ...:     @property
     ...:     def name(self):
     ...:         return self.__name
     ...:     @name.setter
     ...:     def name(self,new_name):
     ...:         self.__name = new_name

In [106]: a_book = Book('magic_book',100000)

In [107]: a_book.name = 'magic_book_2.0'

In [108]: a_book.name
Out[108]: 'magic_book_2.0'

注意这种装饰器写法:name.setter,name 已经被包装为 property 实例,调用实例上的 setter 函数再包装 name 后就会可写。对于 Python 入门者,可以暂时不用太纠结这部分理论,使用 Python 一段时间后,再回过头来自然就会理解。

小结

今天学习 Python 的四大基本数据类型。数值型 int、float 等;容器型 list、dict、tuple、set 等;字符型 str 与正则表达式介绍;自定义类的基本语法规则,class、属性和方法等。

@property @name.setter

Day 3:list 和 tuple 的基本操作、深浅拷贝和切片操作详细等 5 个方面总结

列表

列表(list)作为 Python 中最常用的数据类型之一,是一个可增加、删除元素的可变(mutable)容器。

基本操作

创建 list 的方法非常简单,只使用一对中括号 []。如下创建三个list:

empty = []
lst = [1,'xiaoming',29.5,'17312662388']
lst2 = ['001','2019-11-11',['三文鱼','电烤箱']]

empty 在内存中的示意图:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第9张图片

lst 在内存中的示意图:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第10张图片

lst2 在内存中的示意图:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第11张图片

使用 Python 的内置函数 len 求 list 内元素个数:

len(empty) # 0
len(lst) # 4
len(lst2) # 3

依次遍历 lst 内每个元素并求对应类型,使用 for in 对遍历,内置函数 type 得到类型:

for _ in lst:
    print(f'{_}的类型为{type(_)}')
    
# 注:print中的f格式化输出,引号前加f,变量需要用{}括起来

打印结果如下,列表 lst 内元素类型有 3 种:

1的类型为<class 'int'>
xiaoming的类型为<class 'str'>
29.5的类型为<class 'float'>
17312662388的类型为<class 'str'>

因此,Python 的列表不要求元素类型一致。

如何向 lst2 的第三个元素 ['三文鱼','电烤箱'] 内再增加一个元素 '烤鸭'

首先,使用“整数索引”取出这个元素:

sku = lst2[2] # sku 又是一个列表

sku 变量位于栈帧中,同时指向 lst2[2]:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第12张图片

然后,使用列表的 append 方法增加元素,append 默认增加到 sku列表尾部:

sku.append('烤鸭')
print(sku) # ['三文鱼', '电烤箱', '烤鸭']

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第13张图片

此时想在 sku 指定索引 1 处插入“牛腱子”,使用列表的 insert 方法:

sku.insert(1,'牛腱子')
print(sku) # ['三文鱼', '牛腱子', '电烤箱', '烤鸭']

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第14张图片

在购买烤鸭和牛腱子后,发现超出双十一的预算,不得不放弃购买烤鸭,使用 pop 方法可直接移除列表尾部元素:

item = sku.pop() # 返回烤鸭
print(sku) # ['三文鱼', '牛腱子', '电烤箱']

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第15张图片

发现还是超出预算,干脆移除三文鱼,pop 因为只能移除表尾元素,幸好列表有 remove 方法:

sku.remove('三文鱼') # 更好用:sku.remove(sku[0])
print(sku) # ['牛腱子', '电烤箱']

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第16张图片

深浅拷贝

打印 lst2,发现第三个元素也对应改变,因为 sku 引用 lst2 的第三个元素,sku 指向的内存区域改变,所以 lst2 也会相应改变。

print(lst2) # ['001', '2019-11-11', ['牛腱子', '电烤箱']]

如果不想改变 lst2 的第三个元素,就需要复制出 lst2 的这个元素,列表上有 copy 方法可实现复制:

lst2 = ['001','2019-11-11',['三文鱼','电烤箱']] # 这是lst2的初始值

可视化此行代码,lst2 位于全局帧栈中,其中三个元素内存中的可视化图如下所示:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第17张图片

sku_deep = lst2[2].copy() 

注意,copy 函数,仅仅实现对内嵌对象的一层拷贝,属于 shallow copy。

此时可视化图为如下,因为拷贝 lst2[2],所以 sku_deep 位于栈帧中指向一块新的内存空间:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第18张图片

此时,再对 sku_deep 操作,便不会影响 lst2[2] 的值。

如下修改 sku_deep 的第一个元素(Python 的列表索引从 0 开始编号),lst2 未受到任何影响。

sku_deep[0] = '腱子'
print(lst2[2]) # ['三文鱼','电烤箱']

修改 sku_deep 时,不会影响 lst2[2]。

因为它们位于不同的内存空间中,如图所示,lst2[2] 中的第一个元素依然是“三文鱼”,而不是“腱子”。

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第19张图片

至此,仅仅使用 shallow copy。那么,它与深拷贝,英文叫 deepcopy,又有什么不同?

请看下面例子,a 是内嵌一层 list 的列表,对其浅拷贝生成列表 ac,修改 ac 的第三个元素,也就是列表 [3,4,5] 中的第二个元素为 40:

a = [1,2,[3,4,5]]
ac = a.copy()
ac[0] = 10
ac[2][1] = 40

修改后,分别测试两个值的相等性。

print(a[0] == ac[0])

返回 False,证明实现拷贝。

而 ac[2][1] 是否与原数组 a 的对应位置元素相等:

print(a[2][1] == ac[2][1])

返回 True,进一步证明是浅拷贝,不是深拷贝。

如下图所示:copy 只完成了一层 copy,即 [1,2, id([3,4,5])] 复制一份,而复制后,仍然指向 [3,4,5] 所在的内存空间:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第20张图片

要想实现深度拷贝,需要使用 copy 模块的 deepcopy 函数:

from copy import deepcopy

a = [1,2,[3,4,5]]
ac = deepcopy(a)
ac[0] = 10
ac[2][1] = 40
print(a[0] == ac[0])
print(a[2][1] == ac[2][1])

打印结果,都为 False,结合下图,也能看出内嵌的 list 全部完成复制,都指向了不同的内存区域。

所以,list中内嵌list的拷贝需要用深拷贝。

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第21张图片

切片

Java 和 C++ 中,访问数组中的元素只能一次一个,但 Python 增加切片功能为访问列表带来极大便利。利用内置函数 range(start,stop,step) 生成序列数据,并转为 list 类型:

a = list(range(1,20,3))
print(a) # [1, 4, 7, 10, 13, 16, 19]

使用 a[:3] 获取列表 a 的前三个元素,形象称这类操作为“切片”,切片本身也是一个列表 [1,4,7]:

  • 使用 a[-1] 获取 a 的最后一个元素,返回 int 型,值为 19;
  • 使用 a[:-1] 获取除最后一个元素的切片 [1, 4, 7, 10, 13, 16];
  • 使用 a[1:5] 生成索引为 [1,5)(不包括索引 5)的切片 [4, 7, 10, 13];
  • 使用 a[1:5:2] 生成索引 [1,5) 但步长为 2 的切片 [4,10];
  • 使用 a[::3] 生成索引 [0,len(a)) 步长为 3 的切片 [1,10,19];
  • 使用 a[::-3] 生成逆向索引 [len(a),0) 步长为 3 的切片 [19,10,1]。

逆向:从列表最后一个元素访问到第一个元素的方向。

特别地,使用列表的逆向切片操作,只需一行代码就能逆向列表:

def reverse(lst):
    return lst[::-1]

调用reverse函数:

ra = reverse(a)
print(ra) # [19, 16, 13, 10, 7, 4, 1]

说完列表,还有一个与之很相似的数据类型——元组(tuple)。

元组

元组既然是不可变(immutable)对象,自然也就没有增加、删除元素的方法。

基本操作

使用一对括号(())就能创建一个元组对象,如:

a = () # 空元组对象
b = (1,'xiaoming',29.5,'17312662388')
c = ('001','2019-11-11',['三文鱼','电烤箱'])

它们都是元组,除了 list 是用 [] 创建外,其他都与 list 很相似,比如都支持切片操作。

特别注意:一个整数加一对括号,比如 (10),返回的是整数。必须加一个逗号 (10, ) 才会返回元组对象。

列表和元组都有一个很好用的统计方法 count,实现对某个元素的个数统计:

from numpy import random
a = random.randint(1,5,10) # 从 [1,5) 区间内随机选择 10 个数
at = tuple(a) # 转 tuple:(1, 4, 2, 1, 3, 3, 2, 3, 4, 2)
at.count(3) # 统计 3 出现次数,恰好也为 3 次
可变与不可变

文章开头提到列表是一个可变容器,可变与不可变是一对很微妙的概念。

因为网上经常出现,所以再重点总结下。

创建一个列表 a = [1,3,[5,7],9,11,13],存储示意图:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第22张图片

执行 a.pop() 后删除最后一个元素:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第23张图片

删除后:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第24张图片

再在索引 3 处增加一个元素 8,a.insert(3,8),插入后如下:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第25张图片

因此,对列表而言,因为它能增加或删除元素,所以它是可变的。

但是,如果仅仅在列表 a 中做这一步操作:

a[2].insert(1,6) #在 a[2](也是一个列表)中插入元素 6

插入后可视化图:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第26张图片

对于可变这个概念而言,这就不是真正调整a为可变的操作。

tuple 就是一个典型的不可变容器对象。对它而言,同样也可以修改嵌套对象的取值,但这并没有真正改变 tuple 内的元素。

如下所示,有一个元组 a:

a =(1,3,[5,7],9,11,13)

a 的存储示意图如下:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第27张图片

下面插入一个元素 6:

a[2].insert(1,6)

可以看到,a 内元素没增没减,长度还是 6:

python语言~~特性、基本语法、数据类型、list|tuple深浅拷贝、切片_第28张图片

这就是不可变对象的本质,元组一旦创建后,长度就被唯一确定。

但是,对于 list 而言,列表长度会有增有减,所以它是可变的。

小结

今天总结了:

  • 列表的基本操作
  • 重要深、浅拷贝问题
  • 常见的切片操作
  • 元组(tuple)的基本操作
  • 可变对象,不可变对象

你可能感兴趣的:(#,python)