🧰 Python Toolkit (การแปลภาษาไทยกำลังจะมา) · Python 工具箱

Seven tools you reach for after the basics: classes, generators, decorators, type hints, pathlib, json, and requests. · 学完基础之后最常用的七件工具:类、生成器、装饰器、类型提示、pathlibjsonrequests

You can already read a Python file. You know functions, loops, and lists. This page covers the next seven tools. Each one solves a problem you will meet soon.

Every section has the same shape: the problem, the code, and when to use it. Read the code out loud. Then open the notebook and change it.

การแปลภาษาไทยกำลังจะมา (Thai translation coming soon)

你已经能读懂一个 Python 文件了。你知道函数、循环和列表。本页讲接下来的七件工具。每一件都解决一个你很快会遇到的问题。

每一节的结构都一样:问题是什么代码怎么写什么时候用它。把代码读出声来。然后打开 notebook,动手改一改。

1. Classes — data and behaviour in one place · คลาส · 类 —— 把数据和行为放在一起

Say you are tracking dogs at a shelter. Each dog has a name and an age. Each dog can bark. With plain variables it gets messy fast:

dog1_name = "Somchai"
dog1_age = 3
dog2_name = "Nong Mee"
dog2_age = 5

Two dogs need four variables. Ten dogs need twenty. And nothing links a name to its age. A class fixes this. A class is a template. It says what data one dog holds, and what one dog can do.

class Dog:
    """One dog at the shelter."""

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

    def bark(self):
        return f"{self.name} says woof!"

    def human_years(self):
        return self.age * 7

Now make some dogs. Each one is an object built from the template:

somchai = Dog("Somchai", 3)
nong_mee = Dog("Nong Mee", 5)

print(somchai.bark())         # Somchai says woof!
print(nong_mee.human_years()) # 35

Reading the class

  • class Dog: starts the template. Class names use CapitalCase by convention.
  • __init__ runs once when you make a new dog. It is called the constructor.
  • self means "this particular dog". Every method takes it as the first input.
  • self.name = name stores the name on the object, so it stays there.
  • bark and human_years are methods — functions that belong to the class.
  • You never pass self yourself. somchai.bark() passes it for you.
class Dog the template name age bark() human_years() Dog("Somchai", 3) Dog("Nong Mee", 5) object: somchai name = "Somchai" age = 3 object: nong_mee name = "Nong Mee" age = 5 One template. Many objects. Each object keeps its own data.
A class is written once. Every object made from it carries its own values.

When to use a class

Use a class when some data and some actions always travel together. A dog and its bark. A user and their password check. A file and how you save it.

Do not use a class when a function is enough. Many beginners wrap one function in a class for no reason. If your class has one method and no stored data, write a function instead.

การแปลกำลังจะมา

假设你要记录收容所里的狗。每只狗有名字和年龄。每只狗都会叫。用普通变量很快就乱了:

dog1_name = "Somchai"
dog1_age = 3
dog2_name = "Nong Mee"
dog2_age = 5

两只狗要四个变量。十只狗就要二十个。而且名字和年龄之间没有任何联系。解决了这个问题。类是一个模板。它规定一只狗保存哪些数据、能做哪些事。

class Dog:
    """One dog at the shelter."""

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

    def bark(self):
        return f"{self.name} says woof!"

    def human_years(self):
        return self.age * 7

现在造几只狗。每一只都是用模板造出来的对象

somchai = Dog("Somchai", 3)
nong_mee = Dog("Nong Mee", 5)

print(somchai.bark())         # Somchai says woof!
print(nong_mee.human_years()) # 35

逐行读这个类

  • class Dog: 开始定义模板。类名按惯例用大驼峰命名。
  • __init__ 在新建一只狗时运行一次,叫做构造函数
  • self 表示"当前这一只狗"。每个方法的第一个参数都是它。
  • self.name = name 把名字存在对象上,之后一直都在。
  • barkhuman_years方法 —— 属于这个类的函数。
  • 你从不用自己传 self。写 somchai.bark() 时 Python 会替你传。
class Dog 模板 name age bark() human_years() Dog("Somchai", 3) Dog("Nong Mee", 5) 对象:somchai name = "Somchai" age = 3 对象:nong_mee name = "Nong Mee" age = 5 一个模板,多个对象。每个对象都有自己的数据。
类只写一次。由它创建的每个对象都带着自己的值。

什么时候该用类

当一组数据和一组动作总是同进同出时,就用类。狗和它的叫声。用户和验证密码的方法。文件和保存它的方式。

如果一个函数就够了,就不要用类。很多初学者毫无理由地把一个函数包进类里。如果你的类只有一个方法、也不保存数据,那就写成函数。

2. Generators — yield for data that will not fit · เจนเนอเรเตอร์ · 生成器 —— 用 yield 处理装不下的数据

Here is a function that reads a log file:

def read_lines(path):
    lines = []
    with open(path) as f:
        for line in f:
            lines.append(line.strip())
    return lines          # the WHOLE file is now in memory

This works for a small file. For a 20 GB log file, your computer runs out of memory and the program dies. The problem is return. It hands back everything at once.

A generator hands back one item at a time. You swap return for yield:

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.strip()   # give ONE line, then pause

The code looks almost the same. The behaviour is completely different.

What yield actually does

Calling the function does not run it. It returns a generator object — a promise to produce values later.

lines = read_lines("huge.log")
print(lines)        # generator object, no file read yet

for line in lines:  # NOW it runs, one line at a time
    print(line)

Each time the loop asks for a value, the function runs until it hits yield. Then it pauses, keeping all its variables. The next request wakes it up at that exact spot.

return a list ["line 1", "line 2", "line 3", ... 20 million more ...] all of it, in memory, at once yield one at a time line 1 line 2 line 3 one line in memory at any moment Memory used list: grows with the file generator: flat A 20 GB file and a 20 KB file cost the generator the same.
A list holds every item at once. A generator holds one.

An endless generator

Because values are made on demand, a generator can be infinite. A list cannot.

def count_from(start):
    n = start
    while True:          # never ends
        yield n
        n += 1

for n in count_from(10):
    print(n)
    if n >= 13:
        break            # 10, 11, 12, 13

Rules to remember

  • Any function with yield in it is a generator. There is no other keyword.
  • A generator is used once. Loop over it twice and the second loop gets nothing.
  • You cannot ask for len() or index it with [3]. It does not know its own size.
  • Need a list after all? list(read_lines(path)) — but that brings the memory cost back.

การแปลกำลังจะมา

下面这个函数读取一个日志文件:

def read_lines(path):
    lines = []
    with open(path) as f:
        for line in f:
            lines.append(line.strip())
    return lines          # 整个文件现在都在内存里

文件小的时候没问题。但如果是 20 GB 的日志,内存就会耗尽,程序直接崩溃。问题出在 return 上 —— 它一次性把所有东西都交出去。

生成器一次只交出一项。把 return 换成 yield

def read_lines(path):
    with open(path) as f:
        for line in f:
            yield line.strip()   # 交出一行,然后暂停

代码几乎一模一样,行为却完全不同。

yield 到底做了什么

调用这个函数并不会执行它。它返回一个生成器对象 —— 一个"以后再产生值"的承诺。

lines = read_lines("huge.log")
print(lines)        # 生成器对象,此时还没读文件

for line in lines:  # 现在才开始运行,一次一行
    print(line)

每当循环要一个值,函数就运行到 yield 为止,然后暂停,并保留所有变量。下一次请求会从那个位置继续。

return 一个列表 ["line 1", "line 2", "line 3", ... 还有两千万行 ...] 全部一次性放进内存 yield 一次一项 line 1 line 2 line 3 任何时刻内存里只有一行 内存占用 列表:随文件变大 生成器:不变 20 GB 的文件和 20 KB 的文件, 对生成器来说代价一样。
列表一次装下所有项。生成器只装一项。

无限生成器

因为值是按需产生的,生成器可以是无限的。列表做不到。

def count_from(start):
    n = start
    while True:          # 永远不结束
        yield n
        n += 1

for n in count_from(10):
    print(n)
    if n >= 13:
        break            # 10, 11, 12, 13

要记住的规则

  • 只要函数里有 yield,它就是生成器。没有别的关键字。
  • 生成器只能用一次。循环第二遍时什么都取不到。
  • 不能对它用 len(),也不能用 [3] 取下标。它不知道自己有多长。
  • 确实需要列表?list(read_lines(path)) —— 但内存代价就又回来了。

3. Decorators — @something wraps a function · เดคอเรเตอร์ · 装饰器 —— 用 @something 包住一个函数

You have five functions. You want to know how long each one takes. The obvious fix is to edit all five:

def fetch_data():
    start = time.time()          # added
    ...
    print(time.time() - start)   # added

That is the same two lines copied five times. If you change the format, you edit five places. A decorator writes those lines once and wraps them around any function.

import time

def timed(fn):
    """Print how long the wrapped function takes."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} took {time.time() - start:.2f}s")
        return result
    return wrapper

Now use it. The @ line is the decorator:

@timed
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(2, 3)
# slow_add took 1.00s
# 5

How to read it

A decorator is a function that takes a function and returns a new function. That sentence is the whole idea. The @ is only shorthand:

@timed
def slow_add(a, b): ...

# means exactly the same as:

def slow_add(a, b): ...
slow_add = timed(slow_add)
  • timed(fn) receives the original function as fn.
  • wrapper is the replacement. It does the extra work, then calls fn.
  • *args, **kwargs means "accept any inputs" and pass them straight through.
  • return result matters. Forget it and your function returns None.
  • return wrapper — note the missing brackets. You return the function, not a call to it.
wrapper — added by @timed start = time.time() slow_add(a, b) your real code print(elapsed) runs before runs after The original function is untouched. Only the call goes through the wrapper. slow_add(2, 3) → wrapper(2, 3) → slow_add(2, 3)
The decorator adds a layer around the function. Your code inside stays as it was.

Where you will meet them

You will use decorators from libraries long before you write your own:

DecoratorFromWhat it does
@app.route("/hi")FlaskRuns this function when a browser visits /hi.
@propertyPython itselfLets a method be read like plain data.
@staticmethodPython itselfA method in a class that ignores self.
@lru_cachefunctoolsRemembers past results so repeat calls are instant.
@pytest.fixturepytestSupplies test data to your tests.

การแปลกำลังจะมา

你有五个函数,想知道每个跑多久。最直接的办法是把五个都改一遍:

def fetch_data():
    start = time.time()          # 新增
    ...
    print(time.time() - start)   # 新增

同样两行代码抄了五遍。以后想改格式,就得改五处。装饰器让你只写一次,然后包到任何函数外面。

import time

def timed(fn):
    """Print how long the wrapped function takes."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} took {time.time() - start:.2f}s")
        return result
    return wrapper

然后这样用。带 @ 的那一行就是装饰器:

@timed
def slow_add(a, b):
    time.sleep(1)
    return a + b

slow_add(2, 3)
# slow_add took 1.00s
# 5

怎么读它

装饰器就是一个"接收函数、返回新函数"的函数。这一句话就是全部道理。@ 只是简写:

@timed
def slow_add(a, b): ...

# 和下面完全等价:

def slow_add(a, b): ...
slow_add = timed(slow_add)
  • timed(fn) 把原函数作为 fn 接收进来。
  • wrapper 是替代品。它做额外的事,然后调用 fn
  • *args, **kwargs 表示"接受任意参数",并原样传下去。
  • return result 很关键。忘了写,函数就会返回 None
  • return wrapper —— 注意没有括号。返回的是函数本身,不是调用结果。
wrapper —— 由 @timed 加上 start = time.time() slow_add(a, b) 你真正的代码 print(elapsed) 之前运行 之后运行 原函数一个字都没改。只是调用时多走了一层 wrapper。 slow_add(2, 3) → wrapper(2, 3) → slow_add(2, 3)
装饰器在函数外面加了一层。里面的代码保持原样。

你会在哪里遇到它们

在自己写装饰器之前,你早就会用到别人库里的装饰器:

装饰器来自作用
@app.route("/hi")Flask浏览器访问 /hi 时运行这个函数。
@propertyPython 内置让方法可以像普通数据一样读取。
@staticmethodPython 内置类里不需要 self 的方法。
@lru_cachefunctools记住算过的结果,重复调用瞬间返回。
@pytest.fixturepytest为测试提供数据。

4. Type hints — say what goes in and what comes out · Type hints · 类型提示 —— 说明传什么、返回什么

Look at this function. What is data? A list? A file? A number?

def process(data):
    ...

You cannot tell. You have to read the whole body to find out. Type hints answer the question in the first line:

def greet(name: str) -> str:
    """Return a friendly greeting."""
    return f"Hello, {name}!"
  • name: str — the input should be a string.
  • -> str — the function gives back a string.
  • Nothing else changed. The function body is the same.

Python does not enforce them. greet(42) still runs. Hints are notes for humans and tools, not rules for the interpreter.

The types you will actually use

HintMeans
str, int, float, boolText, whole number, decimal, True/False.
list[str]A list of strings.
dict[str, int]A dictionary with string keys and number values.
str | NoneA string, or nothing. Very common for "might fail".
PathA pathlib path — see the next section.
-> NoneReturns nothing. Use it for functions that only print or save.
from pathlib import Path

def word_count(text: str) -> dict[str, int]:
    """Count how many times each word appears."""
    counts: dict[str, int] = {}
    for word in text.lower().split():
        counts[word] = counts.get(word, 0) + 1
    return counts


def find_config(folder: Path) -> Path | None:
    """Return the config file if it exists, otherwise None."""
    candidate = folder / "config.json"
    return candidate if candidate.exists() else None

Why bother

  • Your editor gets smarter. Type name. and VS Code lists every string method. Without the hint it cannot guess.
  • Mistakes are caught early. Run pip install mypy, then mypy myfile.py. It finds wrong types before you run anything.
  • The code documents itself. -> Path | None warns the reader to handle the None case.
  • AI tools read them. An LLM agent decides how to call your function from its hints and docstring.

Lesson 1 introduces type hints too, from the beginner's side.

การแปลกำลังจะมา

看看这个函数。data 是什么?列表?文件?数字?

def process(data):
    ...

看不出来。你必须把整个函数体读完才知道。类型提示在第一行就回答了这个问题:

def greet(name: str) -> str:
    """Return a friendly greeting."""
    return f"Hello, {name}!"
  • name: str —— 传进来的应该是字符串。
  • -> str —— 函数返回一个字符串。
  • 其他什么都没变。函数体一模一样。

Python 并不强制执行它们。greet(42) 照样能跑。提示是写给人和工具看的说明,不是解释器的规则。

真正常用的几种类型

写法含义
strintfloatbool文本、整数、小数、真/假。
list[str]一个字符串列表。
dict[str, int]键是字符串、值是数字的字典。
str | None一个字符串,或者什么都没有。"可能失败"时非常常用。
Path一个 pathlib 路径 —— 见下一节。
-> None不返回任何东西。只打印或只保存的函数用它。
from pathlib import Path

def word_count(text: str) -> dict[str, int]:
    """Count how many times each word appears."""
    counts: dict[str, int] = {}
    for word in text.lower().split():
        counts[word] = counts.get(word, 0) + 1
    return counts


def find_config(folder: Path) -> Path | None:
    """Return the config file if it exists, otherwise None."""
    candidate = folder / "config.json"
    return candidate if candidate.exists() else None

为什么值得写

  • 编辑器会变聪明。输入 name.,VS Code 就列出所有字符串方法。没有提示它猜不出来。
  • 错误能提早发现。装上 pip install mypy,然后跑 mypy myfile.py。代码还没运行,类型错误就被找出来了。
  • 代码自带说明。-> Path | None 提醒读者要处理 None 的情况。
  • AI 工具会读它们。大模型智能体就是靠类型提示和文档字符串来决定怎么调用你的函数。

第一课也从初学者的角度讲过类型提示。

5. pathlib — file paths that work everywhere · pathlib · pathlib —— 到处都能用的文件路径

Old Python code joins paths with strings:

import os

path = os.path.join(os.path.dirname(__file__), "data", "notes.txt")
name = os.path.splitext(os.path.basename(path))[0]
if not os.path.exists(os.path.dirname(path)):
    os.makedirs(os.path.dirname(path))

It works, but it is hard to read. Every line calls a different os.path function on a plain string. pathlib gives you a real Path object instead:

from pathlib import Path

path = Path(__file__).parent / "data" / "notes.txt"
name = path.stem
path.parent.mkdir(parents=True, exist_ok=True)

Three lines, and each one says what it does.

The slash is the trick

pathlib reuses the / operator to join paths. It reads like a real path:

base = Path("lessons")
today = base / "2026" / "august" / "notes.txt"
print(today)
# Windows:  lessons\2026\august\notes.txt
# Mac/Linux: lessons/2026/august/notes.txt

You write / and Python picks the right separator for the computer. Never type "folder\\file.txt" again.

The methods worth memorising

CodeWhat you get
p.namenotes.txt — the file name with its extension.
p.stemnotes — the name without the extension.
p.suffix.txt — the extension.
p.parentThe folder holding the file.
p.exists()True or False.
p.mkdir(parents=True, exist_ok=True)Make the folder. Do not complain if it is already there.
p.read_text(encoding="utf-8")The whole file as a string. No open() needed.
p.write_text(s, encoding="utf-8")Save a string to the file.
p.glob("*.txt")Every .txt file in the folder. A generator!
p.rglob("*.txt")The same, but through every sub-folder too.
from pathlib import Path

folder = Path("lessons")
for txt in folder.rglob("*.txt"):
    print(txt.stem, "-", len(txt.read_text(encoding="utf-8")), "characters")

การแปลกำลังจะมา

老式的 Python 代码用字符串拼路径:

import os

path = os.path.join(os.path.dirname(__file__), "data", "notes.txt")
name = os.path.splitext(os.path.basename(path))[0]
if not os.path.exists(os.path.dirname(path)):
    os.makedirs(os.path.dirname(path))

能用,但很难读。每一行都在对一个普通字符串调用不同的 os.path 函数。pathlib 给你的是真正的 Path 对象

from pathlib import Path

path = Path(__file__).parent / "data" / "notes.txt"
name = path.stem
path.parent.mkdir(parents=True, exist_ok=True)

三行,而且每一行都在说自己在做什么。

关键在那个斜杠

pathlib/ 运算符借来拼接路径,读起来就像真的路径:

base = Path("lessons")
today = base / "2026" / "august" / "notes.txt"
print(today)
# Windows:  lessons\2026\august\notes.txt
# Mac/Linux: lessons/2026/august/notes.txt

你写 /,Python 会为当前系统选对分隔符。再也不用写 "folder\\file.txt" 了。

值得背下来的方法

代码得到什么
p.namenotes.txt —— 带扩展名的文件名。
p.stemnotes —— 不带扩展名的名字。
p.suffix.txt —— 扩展名。
p.parent文件所在的文件夹。
p.exists()TrueFalse
p.mkdir(parents=True, exist_ok=True)创建文件夹。已存在也不报错。
p.read_text(encoding="utf-8")把整个文件读成字符串。不用写 open()
p.write_text(s, encoding="utf-8")把字符串写进文件。
p.glob("*.txt")文件夹里所有 .txt 文件。是个生成器!
p.rglob("*.txt")同上,但会递归进所有子文件夹。
from pathlib import Path

folder = Path("lessons")
for txt in folder.rglob("*.txt"):
    print(txt.stem, "-", len(txt.read_text(encoding="utf-8")), "characters")

6. json — save and load data in two lines · json · json —— 两行代码存取数据

Your program has a Python dictionary. You want it to still be there tomorrow. JSON is the standard text format for that job. Every language reads it, and every web API speaks it.

import json
from pathlib import Path

student = {
    "name": "Ploy",
    "level": "B1",
    "scores": [82, 91, 77],
    "active": True,
}

# Save it
Path("student.json").write_text(json.dumps(student, indent=2), encoding="utf-8")

# Load it back
loaded = json.loads(Path("student.json").read_text(encoding="utf-8"))
print(loaded["scores"][1])   # 91

That is the whole library, for most work. Two functions.

Four names, easy to mix up

FunctionDirectionRemember it as
json.dumps(obj)Python → textdump-s = dump to string
json.loads(text)text → Pythonload-s = load from string
json.dump(obj, f)Python → open fileno s = works on a file
json.load(f)open file → Pythonno s = works on a file

With pathlib you mostly need only the two s versions, as shown above.

How Python types map to JSON

PythonJSON
dictobject { }
listarray [ ]
strstring
int / floatnumber
True / Falsetrue / false — lower case
Nonenull

Two arguments worth knowing

json.dumps(student, indent=2)                    # readable, one item per line
json.dumps(student, ensure_ascii=False)          # keeps Thai and Chinese readable

Without ensure_ascii=False, the word ครู is saved as ครู. It still loads correctly, but nobody can read the file.

การแปลกำลังจะมา

你的程序里有一个 Python 字典。你希望它明天还在。JSON 就是干这个的标准文本格式。所有语言都能读它,所有 Web API 都说这门话。

import json
from pathlib import Path

student = {
    "name": "Ploy",
    "level": "B1",
    "scores": [82, 91, 77],
    "active": True,
}

# 保存
Path("student.json").write_text(json.dumps(student, indent=2), encoding="utf-8")

# 读回来
loaded = json.loads(Path("student.json").read_text(encoding="utf-8"))
print(loaded["scores"][1])   # 91

对大多数工作来说,这个库就这么多。两个函数。

四个名字,很容易搞混

函数方向怎么记
json.dumps(obj)Python → 文本dump-s = 倒进string
json.loads(text)文本 → Pythonload-s = 从string 读
json.dump(obj, f)Python → 已打开的文件没有 s = 操作文件
json.load(f)已打开的文件 → Python没有 s = 操作文件

配合 pathlib,你基本只需要上面那两个带 s 的版本。

Python 类型和 JSON 的对应关系

PythonJSON
dict对象 { }
list数组 [ ]
str字符串
int / float数字
True / Falsetrue / false —— 小写
Nonenull

两个值得知道的参数

json.dumps(student, indent=2)                    # 好读,每项一行
json.dumps(student, ensure_ascii=False)          # 让中文和泰文保持可读

不写 ensure_ascii=False 的话,ครู 会被存成 ครู。读回来还是对的,但文件没人看得懂。

7. requests — fetch web pages and APIs · requests · requests —— 抓网页、调 API

The first six tools ship with Python. This one you install:

pip install requests

It is the most-used package in Python, and it is worth the install. Here is a whole API call:

import requests

r = requests.get("https://api.github.com/repos/python/cpython", timeout=10)
r.raise_for_status()
data = r.json()

print(data["name"], "has", data["stargazers_count"], "stars")
# cpython has 74328 stars

Reading the four lines

  • requests.get(url) asks a server for something and waits for the answer.
  • timeout=10 gives up after 10 seconds. Always set it. Without it your program can hang forever.
  • raise_for_status() raises an error if the server said 404 or 500. Without it, a failed request looks like a successful one.
  • r.json() turns the reply into Python dictionaries and lists. It runs json.loads for you.

What comes back

CodeWhat you get
r.status_code200 is fine. 404 not found. 500 server broke.
r.okTrue if the status is under 400.
r.textThe reply as a string — use this for HTML pages.
r.json()The reply parsed from JSON — use this for APIs.
r.contentRaw bytes — use this for images and files.

Sending data with the request

# Query string: .../quotes?limit=3
r = requests.get("https://dummyjson.com/quotes", params={"limit": 3}, timeout=10)
for q in r.json()["quotes"]:
    print(f'"{q["quote"]}" — {q["author"]}')

# Sending JSON to a server
r = requests.post(
    "https://httpbin.org/post",
    json={"student": "Ploy", "score": 91},
    headers={"Authorization": "Bearer YOUR_KEY"},
    timeout=10,
)
  • params= builds the ?key=value part safely. Do not glue it on with +.
  • json= sends a Python dictionary as a JSON body. It sets the header for you.
  • headers= is where API keys go, almost always as Authorization.

Saving a file you downloaded

Here requests and pathlib meet:

from pathlib import Path
import requests

r = requests.get("https://www.python.org/static/img/python-logo.png", timeout=30)
r.raise_for_status()
Path("logo.png").write_bytes(r.content)

การแปลกำลังจะมา

前六件工具都是 Python 自带的。这一件需要安装:

pip install requests

它是 Python 里使用最广的包,装它非常值得。下面就是一次完整的 API 调用:

import requests

r = requests.get("https://api.github.com/repos/python/cpython", timeout=10)
r.raise_for_status()
data = r.json()

print(data["name"], "has", data["stargazers_count"], "stars")
# cpython has 74328 stars

逐行读这四行

  • requests.get(url) 向服务器要东西,然后等回应。
  • timeout=10 表示 10 秒后放弃。永远要写它。不写的话程序可能永远卡住。
  • raise_for_status() 在服务器返回 404 或 500 时抛出异常。不写的话,失败的请求看起来跟成功的一样。
  • r.json() 把回应变成 Python 的字典和列表,等于帮你跑了 json.loads

回应里有什么

代码得到什么
r.status_code200 正常,404 找不到,500 服务器出错。
r.ok状态码小于 400 时为 True
r.text回应的字符串形式 —— 抓 HTML 网页时用它。
r.json()按 JSON 解析后的结果 —— 调 API 时用它。
r.content原始字节 —— 下载图片和文件时用它。

随请求一起发送数据

# 查询字符串:.../quotes?limit=3
r = requests.get("https://dummyjson.com/quotes", params={"limit": 3}, timeout=10)
for q in r.json()["quotes"]:
    print(f'"{q["quote"]}" — {q["author"]}')

# 向服务器发送 JSON
r = requests.post(
    "https://httpbin.org/post",
    json={"student": "Ploy", "score": 91},
    headers={"Authorization": "Bearer YOUR_KEY"},
    timeout=10,
)
  • params= 会安全地拼出 ?key=value 部分。别用 + 手工拼。
  • json= 把 Python 字典作为 JSON 请求体发出去,还会自动设置请求头。
  • headers= 是放 API 密钥的地方,几乎总是用 Authorization

保存下载到的文件

这里 requestspathlib 碰上了:

from pathlib import Path
import requests

r = requests.get("https://www.python.org/static/img/python-logo.png", timeout=30)
r.raise_for_status()
Path("logo.png").write_bytes(r.content)

8. All seven in one small program · รวมทั้งเจ็ดอย่าง · 七件工具合成一个小程序

This script fetches quotes from a public API, saves them, and prints them. It uses all seven tools. Read it and name each one.

"""quotes.py — fetch quotes once, then read them from a local file.

    pip install requests
    python quotes.py
"""
import json
import time
from pathlib import Path
from typing import Iterator

import requests

CACHE_DIR = Path("cache")


def timed(fn):                                          # 3. decorator
    """Print how long the wrapped function takes."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        print(f"[{fn.__name__} took {time.time() - start:.2f}s]")
        return result
    return wrapper


class Quote:                                            # 1. class
    """One quote and the person who said it."""

    def __init__(self, text: str, author: str):         # 4. type hints
        self.text = text
        self.author = author

    def short(self, limit: int = 50) -> str:
        """The quote, cut to `limit` characters."""
        if len(self.text) <= limit:
            return self.text
        return self.text[:limit - 3] + "..."


@timed
def fetch_quotes(count: int) -> list[dict]:             # 7. requests
    """Ask the API for `count` quotes."""
    r = requests.get(
        "https://dummyjson.com/quotes",
        params={"limit": count},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["quotes"]


def read_quotes(path: Path) -> Iterator[Quote]:         # 2. generator
    """Read the saved file, one Quote at a time."""
    data = json.loads(path.read_text(encoding="utf-8"))  # 6. json
    for item in data:
        yield Quote(item["quote"], item["author"])


def main() -> None:
    CACHE_DIR.mkdir(exist_ok=True)                      # 5. pathlib
    cache_file = CACHE_DIR / "quotes.json"

    if not cache_file.exists():
        print("No cache. Fetching...")
        quotes = fetch_quotes(5)
        cache_file.write_text(
            json.dumps(quotes, indent=2, ensure_ascii=False),
            encoding="utf-8",
        )
    else:
        print(f"Reading cache: {cache_file}")

    for quote in read_quotes(cache_file):
        print(f"  {quote.short()} — {quote.author}")


if __name__ == "__main__":
    main()

Run it twice. The first run fetches from the internet. The second run reads the file, and is instant. That small change is what a cache is.

การแปลกำลังจะมา

这个脚本从一个公开 API 抓取名言,保存下来,然后打印出来。七件工具全都用上了。读一遍,把每一件都指出来。

"""quotes.py — fetch quotes once, then read them from a local file.

    pip install requests
    python quotes.py
"""
import json
import time
from pathlib import Path
from typing import Iterator

import requests

CACHE_DIR = Path("cache")


def timed(fn):                                          # 3. 装饰器
    """Print how long the wrapped function takes."""
    def wrapper(*args, **kwargs):
        start = time.time()
        result = fn(*args, **kwargs)
        print(f"[{fn.__name__} took {time.time() - start:.2f}s]")
        return result
    return wrapper


class Quote:                                            # 1. 类
    """One quote and the person who said it."""

    def __init__(self, text: str, author: str):         # 4. 类型提示
        self.text = text
        self.author = author

    def short(self, limit: int = 50) -> str:
        """The quote, cut to `limit` characters."""
        if len(self.text) <= limit:
            return self.text
        return self.text[:limit - 3] + "..."


@timed
def fetch_quotes(count: int) -> list[dict]:             # 7. requests
    """Ask the API for `count` quotes."""
    r = requests.get(
        "https://dummyjson.com/quotes",
        params={"limit": count},
        timeout=10,
    )
    r.raise_for_status()
    return r.json()["quotes"]


def read_quotes(path: Path) -> Iterator[Quote]:         # 2. 生成器
    """Read the saved file, one Quote at a time."""
    data = json.loads(path.read_text(encoding="utf-8"))  # 6. json
    for item in data:
        yield Quote(item["quote"], item["author"])


def main() -> None:
    CACHE_DIR.mkdir(exist_ok=True)                      # 5. pathlib
    cache_file = CACHE_DIR / "quotes.json"

    if not cache_file.exists():
        print("No cache. Fetching...")
        quotes = fetch_quotes(5)
        cache_file.write_text(
            json.dumps(quotes, indent=2, ensure_ascii=False),
            encoding="utf-8",
        )
    else:
        print(f"Reading cache: {cache_file}")

    for quote in read_quotes(cache_file):
        print(f"  {quote.short()} — {quote.author}")


if __name__ == "__main__":
    main()

运行两次。第一次会联网抓取,第二次直接读文件,瞬间完成。这个小小的差别就是所谓的缓存。

9. Practice and what to read next · ฝึกต่อ · 练习与后续阅读

Start with the notebook. It has all of this as runnable cells, plus eight exercises with answers.

One project to try: point the capstone script at a different API. Change the URL, the params, and the keys you read from the JSON. Almost nothing else needs to change. That is the point.

การแปลกำลังจะมา

先从 notebook 开始。本页所有内容在里面都是可运行的单元格,另外还有八道带答案的练习。

可以试的一个小项目:把最后那个脚本指向另一个 API。改 URL、改 params、改你从 JSON 里取的键名。其他几乎什么都不用动 —— 这正是重点。