函数 · 15
函数进阶
变量的作用域、返回多个值、把函数当作值来传递。
把函数用得更进一步的三件事:变量住在哪里(作用域)、怎样返回两个值、怎样把函数像值一样传递。
下面的示例一次取回一组数中的最小值和最大值,传入条件函数来数出偶数和不小于 10 的数,并把一共检查了多少次条件记在函数外面的变量里。
checks = 0 # 함수 밖 변수: 프로그램이 끝날 때까지 남아요 def is_even(x): return x % 2 == 0 def is_big(x): return x >= 10 def min_max(nums): lo = hi = nums[0] for x in nums: lo = min(lo, x) hi = max(hi, x) return lo, hi # 두 값을 함께 돌려줘요 def count_if(nums, test): # test에는 함수가 들어와요 global checks # 밖의 변수를 바꾸겠다고 알려요 count = 0 # 함수 안 변수: 부를 때마다 새로 만들어져요 for x in nums: checks += 1 if test(x): count += 1 return count nums = list(map(int, input().split()))lo, hi = min_max(nums)print(lo, hi)print(count_if(nums, is_even))print(count_if(nums, is_big))print(checks)let checks = 0; // 함수 밖 변수: 프로그램이 끝날 때까지 남아요 function isEven(x) { return x % 2 === 0;} function isBig(x) { return x >= 10;} function minMax(nums) { let lo = nums[0], hi = nums[0]; for (const x of nums) { lo = Math.min(lo, x); hi = Math.max(hi, x); } return [lo, hi]; // 두 값을 배열에 담아 돌려줘요} function countIf(nums, test) { // test에는 함수가 들어와요 let count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 for (const x of nums) { checks++; // 밖의 변수를 바꿔요 if (test(x)) count++; } return count;} const nums = require("fs").readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [lo, hi] = minMax(nums);console.log(lo + " " + hi);console.log(countIf(nums, isEven));console.log(countIf(nums, isBig));console.log(checks);import { readFileSync } from "node:fs"; let checks = 0; // 함수 밖 변수: 프로그램이 끝날 때까지 남아요 function isEven(x: number): boolean { return x % 2 === 0;} function isBig(x: number): boolean { return x >= 10;} function minMax(nums: number[]): [number, number] { let lo = nums[0], hi = nums[0]; for (const x of nums) { lo = Math.min(lo, x); hi = Math.max(hi, x); } return [lo, hi]; // 두 값을 배열(튜플)에 담아 돌려줘요} function countIf(nums: number[], test: (x: number) => boolean): number { // test에는 함수가 들어와요 let count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 for (const x of nums) { checks++; // 밖의 변수를 바꿔요 if (test(x)) count++; } return count;} const nums: number[] = readFileSync(0, "utf8").trim().split(/\s+/).map(Number);const [lo, hi] = minMax(nums);console.log(lo + " " + hi);console.log(countIf(nums, isEven));console.log(countIf(nums, isBig));console.log(checks);import java.util.*;import java.util.function.IntPredicate; public class Main { static int checks = 0; // 함수 밖 변수(클래스의 static 필드): 프로그램이 끝날 때까지 남아요 static boolean isEven(int x) { return x % 2 == 0; } static boolean isBig(int x) { return x >= 10; } static int[] minMax(List<Integer> nums) { int lo = nums.get(0), hi = nums.get(0); for (int x : nums) { lo = Math.min(lo, x); hi = Math.max(hi, x); } return new int[] {lo, hi}; // 두 값을 배열에 담아 돌려줘요 } static int countIf(List<Integer> nums, IntPredicate test) { // test에는 함수가 들어와요 int count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 for (int x : nums) { checks++; // 밖의 변수를 바꿔요 if (test.test(x)) count++; } return count; } public static void main(String[] args) { Scanner in = new Scanner(System.in); List<Integer> nums = new ArrayList<>(); while (in.hasNextInt()) nums.add(in.nextInt()); int[] r = minMax(nums); int lo = r[0], hi = r[1]; System.out.println(lo + " " + hi); System.out.println(countIf(nums, Main::isEven)); System.out.println(countIf(nums, Main::isBig)); System.out.println(checks); }}using System;using System.Linq; class Program{ static int checks = 0; // 함수 밖 변수(클래스의 static 필드): 프로그램이 끝날 때까지 남아요 static bool IsEven(int x) { return x % 2 == 0; } static bool IsBig(int x) { return x >= 10; } static (int, int) MinMax(int[] nums) { int lo = nums[0], hi = nums[0]; foreach (int x in nums) { lo = Math.Min(lo, x); hi = Math.Max(hi, x); } return (lo, hi); // 두 값을 튜플로 함께 돌려줘요 } static int CountIf(int[] nums, Func<int, bool> test) // test에는 함수가 들어와요 { int count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 foreach (int x in nums) { checks++; // 밖의 변수를 바꿔요 if (test(x)) count++; } return count; } static void Main() { int[] nums = Console.ReadLine().Split().Select(int.Parse).ToArray(); var (lo, hi) = MinMax(nums); Console.WriteLine(lo + " " + hi); Console.WriteLine(CountIf(nums, IsEven)); Console.WriteLine(CountIf(nums, IsBig)); Console.WriteLine(checks); }}#include <algorithm>#include <functional>#include <iostream>#include <utility>#include <vector> int checks = 0; // 함수 밖 변수(전역 변수): 프로그램이 끝날 때까지 남아요 bool isEven(int x) { return x % 2 == 0;} bool isBig(int x) { return x >= 10;} std::pair<int, int> minMax(const std::vector<int>& nums) { int lo = nums[0], hi = nums[0]; for (int x : nums) { lo = std::min(lo, x); hi = std::max(hi, x); } return {lo, hi}; // 두 값을 pair에 담아 돌려줘요} int countIf(const std::vector<int>& nums, const std::function<bool(int)>& test) { // test에는 함수가 들어와요 int count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 for (int x : nums) { checks++; // 밖의 변수를 바꿔요 if (test(x)) count++; } return count;} int main() { std::vector<int> nums; int v; while (std::cin >> v) nums.push_back(v); auto [lo, hi] = minMax(nums); std::cout << lo << ' ' << hi << '\n'; std::cout << countIf(nums, isEven) << '\n'; std::cout << countIf(nums, isBig) << '\n'; std::cout << checks << '\n';}#include <stdio.h> int checks = 0; // 함수 밖 변수(전역 변수): 프로그램이 끝날 때까지 남아요 int is_even(int x) { return x % 2 == 0;} int is_big(int x) { return x >= 10;} // 돌려줄 값이 둘이라, 결과를 적어 줄 자리(주소)를 받아요void min_max(const int *nums, int n, int *lo, int *hi) { *lo = *hi = nums[0]; for (int i = 0; i < n; i++) { if (nums[i] < *lo) *lo = nums[i]; if (nums[i] > *hi) *hi = nums[i]; }} int count_if(const int *nums, int n, int (*test)(int)) { // test에는 함수(의 주소)가 들어와요 int count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 for (int i = 0; i < n; i++) { checks++; // 밖의 변수를 바꿔요 if (test(nums[i])) count++; } return count;} int main(void) { int nums[100], n = 0; while (n < 100 && scanf("%d", &nums[n]) == 1) n++; int lo, hi; min_max(nums, n, &lo, &hi); printf("%d %d\n", lo, hi); printf("%d\n", count_if(nums, n, is_even)); printf("%d\n", count_if(nums, n, is_big)); printf("%d\n", checks); return 0;}checks = 0; // 함수 밖(맨 위) 변수: 함수 안에서도 같은 변수예요 isEven(x){ 返回 x % 2 == 0;} isBig(x){ 返回 x >= 10;} minMax(nums){ lo = nums[0]; hi = nums[0]; 遍历 (x 在 nums) { lo = 数学.较小值(lo, x); hi = 数学.较大值(hi, x); } 返回 [lo, hi]; // 두 값을 목록에 담아 돌려줘요} countIf(nums, test){ count = 0; // 함수 안 변수: 부를 때마다 새로 만들어져요 遍历 (x 在 nums) { checks++; // 밖의 변수를 바꿔요 如果 (test(x)) { count++; } } 返回 count;} nums = 控制台.读数字();r = minMax(nums);控制台.输出(r[0] + " " + r[1]);控制台.输出(countIf(nums, isEven));控制台.输出(countIf(nums, isBig));控制台.输出(checks);4 15 8 23 6 7
4 23 3 2 12
要点
- 变量有自己住的地方(作用域)。
在函数里创建的变量只能在这个函数里用,每次调用都会重新创建。
示例中的count每次调用都从 0 重新开始。 - 在函数外面创建的变量会一直保留到程序结束。
示例中的checks在两次调用之间保持着值,最后是 12。 - 在函数里修改外面的变量,各语言的做法不同。
外面的变量如果到处被修改,就很难找到是哪里改的。
所以只把它用在像示例的checks这样全程序只有一份的记录上,需要的值用参数接收,count这样的计算结果用return交回。 - 要返回两个值,就把它们打包成一个整体返回,接收的一方再拆开。
用来打包的容器(元组、数组、pair、列表)因语言而异。
C 则是接收用来写入结果的地址。 - 函数也是值,所以可以传给别的函数。
示例只用一个计数函数,就完成了“数偶数”和“数不小于 10 的数”。
只把不同的部分(条件)抽成函数传进去。 - 传递时不加括号,只写名字。
is_even是函数本身,is_even(4)是调用函数得到的结果。
各语言的写法
| Python | 在函数里只读外面的变量时直接用;要修改时,在函数开头写 global checks。两个值用 return lo, hi(元组)返回,用 lo, hi = min_max(nums) 接收。函数像 count_if(nums, is_even) 这样只传名字,简短的条件可以用 lambda x: x >= 10 当场写出 |
|---|---|
| JavaScript | 在函数外面用 let 创建的变量,在函数里不用任何标记就能读写。在函数里用 let 再创建同名变量,它就是和外面不同的另一个变量。两个值用 return [lo, hi]; 返回,用 const [lo, hi] = minMax(nums); 接收。函数像 countIf(nums, isEven) 这样传递,简短的条件用箭头函数 (x) => x >= 10 |
| TypeScript | 和 JavaScript 一样。 返回两个值的类型写作 [number, number],接收函数的参数写作 test: (x: number) => boolean |
| Java | 类的外面不能放变量,所以函数外面的变量写成类的 static 字段(static int checks = 0;)。两个值放进 int[],或者定义 record MinMax(int lo, int hi) {} 来返回。函数用 IntPredicate test 接收,用 test.test(x) 调用;传递时写 Main::isEven 或 lambda x -> x >= 10。在 lambda 里修改 main 的局部变量会编译错误 |
| C# | 类的外面不能放变量,所以函数外面的变量写成类的 static 字段。两个值用元组 (int, int),return (lo, hi);,接收时写 var (lo, hi) = MinMax(nums);。也可以用 out 参数(out int lo)接收。函数用 Func<int, bool> test 接收,传递时写 IsEven 或 lambda x => x >= 10 |
| C++ | 写在函数外面的变量是全局变量,任何函数都能读写。 两个值用 std::pair<int, int>,return {lo, hi};,接收时写 auto [lo, hi] = minMax(nums);。函数用 std::function<bool(int)>(<functional>)接收,传递时写 isEven 或 lambda [](int x) { return x >= 10; } |
| C | 写在函数外面的变量是全局变量。 没有返回两个值的语法,所以像示例那样接收用来写入结果的地址( int *lo, int *hi,调用时写 &lo, &hi),或者放进结构体返回。函数用函数指针 int (*test)(int) 接收,像 count_if(nums, n, is_even) 这样传名字。没有 lambda |
| Craykon | 在最外层创建的变量在函数里也是同一个变量,不用任何标记就能读写。 在函数里第一次创建的变量只能在这个函数里用。 两个值像 返回 [lo, hi]; 这样放进列表返回,用 r[0]、r[1] 取出。没有 lambda,所以像 countIf(nums, isEven) 这样传入有名字的函数 |
常见错误
想用 return lo, hi; 返回两个值JavaScript · C · C++
function minMax(nums) {
let lo = nums[0], hi = nums[0];
for (const x of nums) { lo = Math.min(lo, x); hi = Math.max(hi, x); }
return lo, hi;
}
console.log(minMax([4, 15, 8]));→ 15 — 逗号运算符只留下后面的值,所以只返回了 hi。C、C++ 也只给出警告就能编译,结果同样是 15。TypeScript 会在类型检查时报错(TS2695)拦住
JavaScript 写 return [lo, hi];,C++ 用 std::pair 写 return {lo, hi};,C 接收地址或用结构体返回。return lo, hi 能返回两个值的是 Python。
没写 global 就修改外面的变量Python
checks = 0
def check(x):
checks += 1
return x % 2 == 0
print(check(4))→ UnboundLocalError: cannot access local variable 'checks' where it is not associated with a value — 在函数里被赋值的名字会被当作这个函数自己的变量,于是要给还没有值的 checks 加 1 时就停下了
在函数开头写 global checks。
只读的时候不需要。
函数里的变量和最外层的变量同名Craykon
total = 100;
twice(x)
{
total = x * 2;
返回 total;
}
控制台.输出(twice(4));
控制台.输出(total);→ 8、8 — 函数没有创建新变量,而是改了最外层的 total,100 不见了
只在函数里用的变量,要取和最外层变量不同的名字。
参数即使同名,也是这个函数自己的变量。
在其他语言里,在函数里用 let total、int total 新建(Python 是不写 global 直接赋值),得到的是和外面不同的变量,输出是 8、100。
各语言的差异
传递函数时如果像
JavaScript 会以
TypeScript 在类型检查时指出,Craykon 则报错说 isEven 需要 1 个值却给了 0 个。
传递函数时如果像
countIf(nums, isEven()) 这样加了括号,就不是传递,而是当场调用了。JavaScript 会以
TypeError: test is not a function 停止,Python 会以 TypeError: is_even() missing 1 required positional argument: 'x' 停止。TypeScript 在类型检查时指出,Craykon 则报错说 isEven 需要 1 个值却给了 0 个。