目录
- 既能面向对象,又能面向过程
- 没有 main() 函数
- 函数定义与类定义语句也会被执行
- 使用缩进而不是花括号 {} 区分代码段的层级
- 语句末尾有没有分号都行(误)
- 字符串可以用单引号,也可以用双引号,还可以用三重引号
- 向下取整除法运算符 // 和幂运算符 **
- pass语句
- 没有switch-case语句
- for循环不需要计数器
- for、while、try语句支持else子句
- 任何非零整数都为真,零为假
- 灵活的import语句
- 运算符 is 和 is not
- 对象中没有私有属性
- 文档字符串
- 默认支持UTF-8
对于新手而言,官方的 Python 教程 是一个非常不错的学习资源,内容安排由浅入深。但对于已经有其他语言编程经验的人来说,学习Python更快的方法可能是先 “找不同”。Python某些特性确实非常 “特别”,甚至会让人惊呼 “还可以这样?” 这里简单罗列一些这样的特性。
既能面向对象,又能面向过程
Python本质上是一门面向对象的语言,即使 int
等内置的基本数据类型都是 class,但使用Python既可以进行面向对象的编程,也可以进行面向过程的编程,甚至能够像Shell脚本一样使用,整个python脚本里可以既没有类定义也没有函数定义。例如:
# helloworld.py
# shell-script style
s1 = "Hello %s."
s2 = "world"
print(s1 % s2)
# helloworld.py
# 面向过程编程
def foo(s):
return "Hello %s." % s
if __name__ == "__main__":
s1 = foo("world")
print(s1)
# helloworld.py
# 面向对象编程
class Hello:
def say_hello_to(self, s):
print("Hello %s." % s)
if __name__ == "__main__":
hello = Hello()
hello.say_hello_to("world")
没有 main() 函数
你可能已经注意到,上面三种形式的 helloworld.py
都没有main()函数。但它们都可以通过 python helloworld.py
的方式执行,并输出相同的内容。后面两种形式的代码中,都有 if __name__ == "__main__":
的代码块,当 .py
文件以脚本形式被执行时,内置变量 __name__
会被赋值为 "__main__"
,从而此段代码会被执行。但当 .py
文件被以模块形式导入时,__name__
会被赋值为模块名称,相应的,这段代码就不会执行。
我们加上一行查看 __name__
的语句:
# helloworld.py
class Hello:
def say_hello_to(self, s):
print("Hello %s." % s)
# 查看内部变量 __name__ 的值
print("__name__ is '" + __name__ + "'\n")
if __name__ == "__main__":
hello = Hello()
hello.say_hello_to("world")
以脚本方式执行:
C:\Users\user\PyProjects>python helloworld.py
__name__ is '__main__'
Hello world.
以模块方式导入:
C:\Users\user\PyProjects>python
Python 3.10.4 (tags/v3.10.4:9d38120, Mar 23 2022, 23:13:41) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import helloworld
__name__ is 'helloworld'
>>>
函数定义与类定义语句也会被执行
上面的最后一个例子中,输出 __name__
的语句既没有放在 __main__
代码段中,也没有放在类的方法中,而是放到了类定义之后、'__main__'
代码段之前的 “顶级” 代码的位置。那么,把这行代码放在最开始或者最后面,它还会执行吗?
事实上,当 .py
文件被加载时,文件中所有的语句都会被顺序执行,包括函数定义 def
语句和类定义 class
语句。理解了这一点,即使你看到某个文件里出现了两段 '__main__'
代码段,也不会觉得奇怪了。
使用缩进而不是花括号 {} 区分代码段的层级
前面的例子已经显而易见。关于缩进,在《Python语言参考》的词法分析一章中有专门的说明。不过一般而言,关于缩进只要记住下面三点:
- 不要使用
tab
- 理论上只要缩进量保持一致即可,无论是1个空格还是8个空格
- IDE会自动将
tab
转换为4个空格
关于编码风格的重要提示:
https://docs.python.org/zh-cn/3/tutorial/controlflow.html#intermezzo-coding-style
Python 项目大多都遵循 PEP 8 的风格指南;它推行的编码风格易于阅读、赏心悦目。Python 开发者均应抽时间悉心研读;以下是该提案中的核心要点:
- 缩进,用 4 个空格,不要用制表符。
4 个空格是小缩进(更深嵌套)和大缩进(更易阅读)之间的折中方案。制表符会引起混乱,最好别用。- 换行,一行不超过 79 个字符。
这样换行的小屏阅读体验更好,还便于在大屏显示器上并排阅读多个代码文件。- 用空行分隔函数和类,及函数内较大的代码块。
- 最好把注释放到单独一行。
- 使用文档字符串。
- 运算符前后、逗号后要用空格,但不要直接在括号内使用: a = f(1, 2) + g(3, 4)。
- 类和函数的命名要一致;按惯例,命名类用 UpperCamelCase,命名函数与方法用 lowercase_with_underscores。命名方法中第一个参数总是用 self (类和方法详见 初探类)。
- 编写用于国际多语环境的代码时,不要用生僻的编码。Python 默认的 UTF-8 或纯 ASCII 可以胜任各种情况。
- 同理,就算多语阅读、维护代码的可能再小,也不要在标识符中使用非 ASCII 字符。
语句末尾有没有分号都行(误)
前面的例子中,我们在语句末尾都没有分号,这也是正确的写法。但如果我们在语句末尾加上分号,程序也不会出错。比如像下面这样:
# helloworld.py
class Hello:
def say_hello_to(self, s):
print("Hello %s." % s);
if __name__ == "__main__":
hello = Hello();
hello.say_hello_to("world");
事实上,使用分号可以将多条简单语句组合成一条复合语句,例如:
if __name__ == "__main__":
# 增加一行调试语句
import ipdb; ipdb.set_trace()
hello = Hello()
hello.say_hello_to("world")
但是,作为一个合格的python程序员,请尽量 不要使用分号。
字符串可以用单引号,也可以用双引号,还可以用三重引号
Python的字符串对应的数据类型为 str
,既可以使用单引号'
,也可以使用双引号 "
。区别在于:用单引号时,字符串里的双引号无需转义;而使用双引号时,字符串里的单引号无需转义。
>>> s1 = 'Using \''
>>> s2 = "Using '"
>>> s3 = "Using \'"
>>> s1 == s2 == s3
True
这也意味着:'A'
是一个长度为1的字符串,而不是一个字符。
另外,Python中还可以使用三重引号 '''
或 """
来表示字符串,它的优势在于可以自动包括换行符,而无需转义。
>>> s1 = 'one line\nanother line'
>>> s2 = """one line
... another line"""
>>> s1 == s2
True
>>> print(s1)
one line
another line
>>> print(s2)
one line
another line
>>>
向下取整除法运算符 // 和幂运算符 **
Python提供一种特殊的除法运算符 //
,含义为向下取整除法。
>>> 15/4
3.75
>>> 15//4
3
>>> -11/4
-2.75
>>> -11//4
-3
>>>
Python的幂运算符也与其他很多语言不同,不是 ^
而是 **
。
>>> 2**3
8
>>> 5**2
25
>>> 2**-1
0.5
Python中的 ^
是异或运算符,例如:
>>> bin(0b1001 ^ 0b1111)
'0b110'
>>> hex(0x1 ^ 0xF)
'0xe'
>>> set1 = {1, 2, 3}
>>> set2 = {3, 4, 1}
>>> set1 ^ set2
{2, 4}
pass语句
pass
语句不执行任何操作。语法上需要一个语句,但程序不实际执行任何动作时,可以使用该语句。
class MyClass:
# TODO: ....
pass
def foo(bar):
# TODO: ....
pass
for item in my_list:
# TODO: ....
pass
没有switch-case语句
直到3.10,Python才出现了与switch-case语句类似的match语句。在3.10之前,可以用 if-elif-else
语句实现类似的逻辑,但有时候也可以使用一种更python的方式——字典——像下面这样:
animal_dict = {
1: "Dog",
2: "Cat",
3: "Rabbit",
}
def getAnimal(animal_id):
return animal_dict.get(animal_id, "Invalid Animal")
或者这样:
def Animal1():
return "Dog"
def Animal2():
return "Cat"
def Animal3():
return "Rabbit"
def DefaultAnimal():
return "Invalid Animal"
animal_dict = {
'A': Animal1,
'B': Animal2,
'C': Animal3
}
def getAnimal(animal_id):
foo = animal_dict.get(animal_id, DefaultAnimal)
return foo()
for循环不需要计数器
Python里的许多数据类型都可以用for循环遍历,例如 list
(列表)、tuple
(元组)、 dict
(字典)、 range
(范围)、 str
(字符串)、 bytes
(字节串)、 set
(集合)等。
# 列表
my_list = ['A', 'B', 'C', 'D']
for item in my_list:
print(item)
# 元组
my_tuple = ('1', '2', 3, 4)
for item in my_tuple:
print(item)
# 集合
my_set = {'dog', 'cat', 'rabbit'}
for item in my_set:
print(item)
class MyClass:
pass
# 字典
my_dict = {
'A': 10,
2: MyClass(),
'bar': (1, 2)
}
for k,v in my_dict.items():
print(repr(k), repr(v))
# 范围
my_range = range(10,15)
for item in my_range:
print(item)
# 字符串
my_string = "This is a string."
for item in my_string:
print(item)
# 字节串
my_bytes = b"This is a string."
for item in my_bytes:
print(chr(item))
for、while、try语句支持else子句
for
循环的 else
子句(如果有)在循环项耗尽时执行并终止循环(break
语句终止循环且不执行 else
子句体,continue
语句将在执行时将跳过子句体中的剩余部分并转往下一项继续执行,或者在没有下一项时转往 else
子句执行):
>>> for n in range(2, 10):
... for x in range(2, n):
... if n % x == 0:
... print(n, 'equals', x, '*', n//x)
... break
... else:
... # loop fell through without finding a factor
... print(n, 'is a prime number')
...
2 is a prime number
3 is a prime number
4 equals 2 * 2
5 is a prime number
6 equals 2 * 3
7 is a prime number
8 equals 2 * 4
9 equals 3 * 3
while
循环的 else
子句(如果有)在while条件为 False
时执行并终止循环( break
语句在执行时将终止循环且不执行 else
子句体。 continue
语句在执行时将跳过子句体中的剩余部分并返回检验while条件表达式):
>>> x = 0
>>> while x < 100:
... if x % 20 == 0:
... print(x)
... x += 1
... else:
... print("That's all.")
...
0
20
40
60
80
That's all.
try ... except
语句具有可选的 else
子句,该子句如果存在,它必须放在所有 except
子句 之后。 它适用于 try
子句 没有引发异常但又必须要执行 的代码。(参考:错误和异常)
以下例子能够看出 else
和 finally
的区别:
>>> def divide(x, y):
... try:
... result = x / y
... except ZeroDivisionError:
... print("division by zero!")
... else:
... print("result is", result)
... finally:
... print("executing finally clause")
...
>>> divide(3, 1)
result is 3.0
executing finally clause
>>> divide(3, 0)
division by zero!
executing finally clause
>>> divide(3, 'a')
executing finally clause
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in divide
TypeError: unsupported operand type(s) for /: 'int' and 'str'
>>>
任何非零整数都为真,零为假
这一点与C语言一样。进行条件判断时,条件也可以是字符串或列表的值,事实上,任何序列都可以;长度非零就为真,空序列则为假。举例而言,对于字符串类型的变量 x
,下面两种写法是等价的:
x = str(some_var)
if x:
# do something
x = str(some_var)
if x != None and x != '':
# do something
需要注意,bool
类型继承自 int
,True == 1
和 False == 0
均成立,但 False == ''
、False == []
以及 True == 2
等等均不成立。
>>> True == 1
True
>>> False == 0
True
>>> False == ''
False
>>> False == []
False
>>> False == None
False
灵活的import语句
Python中的 import
语句类似其他语言的 #include
,用于导入其他模块中的名称,但import语句的写法更加多样。
可以直接import:
>>> import math
>>> math.ceil(10/3)
4
使用 import item.subitem.subsubitem 句法时,除最后一项外,每个 item 都必须是包;最后一项可以是模块或包,但不能是上一项中定义的类、函数或变量。
>>> import django.utils.encoding.iri_to_uri
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ModuleNotFoundError: No module named 'django.utils.encoding.iri_to_uri'; 'django.utils.encoding' is not a package
>>> import django.utils.encoding
>>> django.utils.encoding.iri_to_uri('中文')
'%E4%B8%AD%E6%96%87'
可以导入模块中的一个或几个名称(通过这种方式导入 item 时,item 可以是包的子模块(或子包),也可以是包中定义的函数、类或变量等其他名称。import 语句首先测试包中是否定义了 item;如果未在包中定义,则假定 item 是模块,并尝试加载。如果找不到 item,则触发 ImportError 异常):
>>> from math import pi,degrees
>>> pi
3.141592653589793
>>> degrees(pi)
180.0
可以给导入的名称改名:
>>> from decimal import Decimal as D
>>> num = D(1.245)
>>> num
Decimal('1.24500000000000010658141036401502788066864013671875')
虽然也支持使用 *
导入全部名称,但是官方不提倡在生产代码中使用这种做法。
>>> from decimal import *
>>> getcontext().prec = 6
>>> Decimal(1) / Decimal(7)
Decimal('0.142857')
>>> getcontext().prec = 28
>>> Decimal(1) / Decimal(7)
Decimal('0.1428571428571428571428571429')
运算符 is 和 is not
运算符 is 和 is not 用于检测对象的标识号:当且仅当 x 和 y 是同一对象时 x is y 为真。 一个对象的标识号可使用 id() 函数来确定。 x is not y 会产生相反的逻辑值。
>>> class A:
... pass
...
>>> a = A()
>>> b = A()
>>> a is not b
True
>>> c = a
>>> a is c
True
>>> id(a)
1984982945168
>>> id(b)
1984983270208
>>> id(c)
1984982945168
>>> x = None
>>>> x == None
True
>>> x is None
True
>>> id(x)
140717859773656
>>> id(None)
140717859773656
对象中没有私有属性
Python中将所有形如 a.b
的名称中的 b
称为属性(Attribute),在类对象中,属性引用 obj.name
中的 name
就是类对象的属性。类对象的属性可以是一个变量,也可以是一个方法/函数。但是在Python中,所有类对象的属性都不是 “私有” 的。
https://docs.python.org/zh-cn/3/tutorial/classes.html#private-variables
那种仅限从一个对象内部访问的“私有”实例变量在 Python 中并不存在。 但是,大多数 Python 代码都遵循这样一个约定:带有一个下划线的名称 (例如 _spam) 应该被当作是 API 的非公有部分 (无论它是函数、方法或是数据成员)。 这应当被视为一个实现细节,可能不经通知即加以改变。
由于存在对于类私有成员的有效使用场景(例如避免名称与子类所定义的名称相冲突),因此存在对此种机制的有限支持,称为 名称改写。 任何形式为 __spam 的标识符(至少带有两个前缀下划线,至多一个后缀下划线)的文本将被替换为 _classname__spam,其中 classname 为去除了前缀下划线的当前类名称。 这种改写不考虑标识符的句法位置,只要它出现在类定义内部就会进行。
下面举例说明这种情况:
# demo.py
# 父类
class A:
x = 1
_y = 2
__z = 3
def foo1(self, v):
print("foo1(%d) is called." % v)
def _foo2(self, v):
print("_foo2(%d) is called." % v)
def __foo3(self, v):
print("__foo3(%d) is called." % v)
# 子类
class B(A):
pass
if __name__ == "__main__":
# 创建子类对象
b = B()
b.foo1(b.x)
b._foo2(b._y)
b._A__foo3(b._A__z)
try:
b.__foo3()
except AttributeError as e:
print(repr(e))
C:\Users\user\PyProjects>python demo.py
foo1(1) is called.
_foo2(2) is called.
__foo3(3) is called.
AttributeError("'B' object has no attribute '__foo3'")
文档字符串
三重引号字符串通常用作文档字符串(docstring)。以上面演示Python没有 “私有” 属性的代码为例,我们加上docstring。
# demo.py
# 父类
class A:
"""
Demo Class for "private" attributes.
This is the Base Class.
"""
x = 1
_y = 2
__z = 3
def foo1(self, v):
"""
The public method. print a message with v.
Parameters:
v (int): Any int number.
Return:
None
"""
print("foo1(%d) is called." % v)
def _foo2(self, v):
"""
The method that should not call.
"""
print("_foo2(%d) is called." % v)
def __foo3(self, v):
"""
The "private" method.
"""
print("__foo3(%d) is called." % v)
# 子类
class B(A):
"""
Demo Class for "private" attributes.
This is the Sub Class.
"""
pass
if __name__ == "__main__":
# 创建子类对象
b = B()
b.foo1(b.x)
b._foo2(b._y)
b._A__foo3(b._A__z)
try:
b.__foo3()
except AttributeError as e:
print(repr(e))
然后我们在Python解释器中执行 help()
命令,看看是什么效果。
>>> import demo
>>> help(demo.A)
Help on class A in module demo:
class A(builtins.object)
| Demo Class for "private" attributes.
|
| This is the Base Class.
|
| Methods defined here:
|
| foo1(self, v)
| The public method. print a message with v.
|
| Parameters:
| v (int): Any int number.
|
| Return:
| None
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| x = 1
可以看到,_foo2()
和 __foo3()
的 docstring 都没有显示,不仅如此, _y
和 __z
这两个成员变量也没有显示。
查看 __doc__
变量:
>>> print(demo.A.__doc__)
Demo Class for "private" attributes.
This is the Base Class.
>>> print(demo.A.foo1.__doc__)
The public method. print a message with v.
Parameters:
v (int): Any int number.
Return:
None
>>> print(demo.A._foo2.__doc__)
The method that should not call.
>>> print(demo.A.__foo3.__doc__)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: type object 'A' has no attribute '__foo3'
>>> print(demo.A._A__foo3.__doc__)
The "private" method.
>>>
再看看 class B 的情况:
>>> help(demo.B)
Help on class B in module demo:
class B(A)
| Demo Class for "private" attributes.
|
| This is the Sub Class.
|
| Method resolution order:
| B
| A
| builtins.object
|
| Methods inherited from A:
|
| foo1(self, v)
| The public method. print a message with v.
|
| Parameters:
| v (int): Any int number.
|
| Return:
| None
|
| ----------------------------------------------------------------------
| Data descriptors inherited from A:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes inherited from A:
|
| x = 1
使用pydoc命令:
C:\Users\user\PyProjects>python -m pydoc hello.demo
Help on module hello.demo in hello:
NAME
hello.demo
DESCRIPTION
# demo.py
CLASSES
builtins.object
A
B
class A(builtins.object)
| Demo Class for "private" attributes.
|
| This is the Base Class.
|
| Methods defined here:
|
| foo1(self, v)
| The public method. print a message with v.
|
| Parameters:
| v (int): Any int number.
|
| Return:
| None
|
| ----------------------------------------------------------------------
| Data descriptors defined here:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| x = 1
class B(A)
| Demo Class for "private" attributes.
|
| This is the Sub Class.
|
| Method resolution order:
| B
| A
| builtins.object
|
| Methods inherited from A:
|
| foo1(self, v)
| The public method. print a message with v.
|
| Parameters:
| v (int): Any int number.
|
| Return:
| None
|
| ----------------------------------------------------------------------
| Data descriptors inherited from A:
|
| __dict__
| dictionary for instance variables (if defined)
|
| __weakref__
| list of weak references to the object (if defined)
|
| ----------------------------------------------------------------------
| Data and other attributes inherited from A:
|
| x = 1
FILE
c:\users\user\pyprojects\hello\demo.py
默认支持UTF-8
与 Python 2 不同,Python 3 默认支持UTF-8编码,如果 .py
文件以 UTF-8
编码保存,则无需额外声明。如果是以其他编码方式保存,则需要在文件最开始声明编码,例如:
# -*- coding: gb18030 -*-
print("这是一个GB18030编码的文件")
相反,在 Python 2 中,如果使用 UTF-8
编码,则一定要在文件最开始声明:
# -*- coding: utf-8 -*-
# Here is your code.
如何查看和修改文件编码?
VSCode右下角,不仅可以查看文件编码,还可以对编码进行转换。