news 2026/8/17 20:45:10

NumPy zeros_like函数详解:从基础原理到工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NumPy zeros_like函数详解:从基础原理到工程实践

1. 从“零”开始:为什么我们需要一个“长得像”的数组生成器?

在数据科学和数值计算的日常里,我们经常遇到一个场景:手头有一个现成的数组A,它的形状、数据类型(dtype)都定义好了,现在我需要创建一个新的数组B,要求BA在内存布局、形状、数据类型上完全一致,但所有元素的值都初始化为零。这个需求听起来简单,但如果你手动去实现,会发现有不少细节需要处理。

最直接的想法可能是np.zeros(A.shape, dtype=A.dtype)。这确实能解决大部分问题。但数组的世界远不止shapedtype。比如,A可能是一个 Fortran 风格(列优先)的数组,而zeros默认创建的是 C 风格(行优先)的。在涉及大规模矩阵运算或与某些底层库(如某些线性代数库)交互时,内存布局的差异可能导致性能下降甚至错误。再比如,A可能是一个结构化数组(structured array)或者是一个记录数组(recarray),它的dtype是一个包含多个字段的复杂描述,手动复制这个dtype描述符不仅繁琐,还容易出错。

np.zeros_like()函数就是为了优雅、精确地解决这类问题而生的。它的核心设计哲学是“模仿”:你给我一个原型(prototype)数组,我返回一个在元数据上与其完全一致,但数据全部为零的新数组。这里的“元数据”是一个宽泛的概念,至少包括:

  • 形状(shape)
  • 数据类型(dtype)
  • 内存布局(order, ‘C’ 或 ‘F’)
  • 子类型(subtype),例如是np.ndarray还是np.matrix(尽管后者已不推荐使用)

这个函数将我们从手动匹配这些属性的琐碎工作中解放出来,保证了代码的简洁性和鲁棒性。尤其是在编写通用函数或库时,你无法预知用户会传入什么样的数组,zeros_like提供了一种标准化的、安全的方式来创建与之兼容的零数组。

2. 基石:np.zeros函数的深度剖析

要透彻理解zeros_like,我们必须先夯实它的基石——np.zeros。很多人觉得np.zeros(shape)就是全部,其实不然,它的可选参数里藏着大学问。

2.1 基本语法与核心参数

numpy.zeros(shape, dtype=float, order='C', *, like=None)

  • shape: 这是唯一必需的参数。它可以是一个整数(如5, 创建一维数组),也可以是一个表示维度的整数元组(如(3, 4), 创建3行4列的二维数组)。
  • dtype: 数据类型,默认为np.float64。这是初学者最容易忽略但至关重要的参数。数组的内存分配和计算效率很大程度上取决于dtype
    • dtype=float(或np.float64): 每个元素占8字节,适用于高精度浮点数计算。
    • dtype=np.float32: 每个元素占4字节,在深度学习、图形处理中广泛应用,能节省近一半内存,有时计算也更快。
    • dtype=int(或np.int64): 创建整数零数组。注意,Python的int类型会被映射到np.int64
    • dtype=np.uint8: 创建无符号8位整数数组,元素范围0-255,常用于图像数据。
    • dtype=bool: 创建布尔数组,所有元素为False(在NumPy中,False等价于0)。
    • dtype=complex: 创建复数数组,实部和虚部均为0.0。
    • dtype=object: 创建Python对象数组,每个元素都是None。这种数组失去了NumPy的数值计算性能优势,应谨慎使用。
  • order: 内存布局顺序,默认为'C'(C风格,行优先)。另一个选项是'F'(Fortran风格,列优先)。这个参数在以下情况尤为重要:
    • 性能优化: 当你需要对数组进行列方向为主的连续操作时(例如,某些线性代数运算),使用order='F'创建的数组可能更快,因为它更好地利用了CPU缓存。
    • 接口兼容: 与某些期望列优先数组的旧式Fortran或C++库交互时。
    • 重塑(reshape)操作: 内存布局会影响reshape等操作的行为和效率。
  • like: 这是一个较新的参数(NumPy 1.20+),用于实现数组API兼容性。它允许你传入一个“类似数组”的对象(即实现了__array__方法的对象),zeros函数会返回一个与该对象类型一致的新数组。例如,传入一个CuPy数组(GPU数组),zeros会返回一个也在GPU上的零数组。这是迈向统一数组生态的重要一步。

2.2 不同dtype下的内存与计算考量

选择正确的dtype不是风格问题,而是效率和正确性问题。

场景一:图像处理假设你处理一张1000x1000的灰度图。如果错误地使用float64

img_float64 = np.zeros((1000, 1000), dtype=np.float64) # 占用内存:1000*1000*8 bytes ≈ 7.63 MB

而实际上,图像像素值通常在0-255,用uint8足矣:

img_uint8 = np.zeros((1000, 1000), dtype=np.uint8) # 占用内存:1000*1000*1 bytes ≈ 0.95 MB

内存占用相差8倍!在批量处理或移动设备上,这个差异是致命的。

场景二:深度学习模型初始化在训练神经网络时,权重初始化通常使用float32,因为大部分GPU和深度学习框架(如TensorFlow, PyTorch)对float32有最好的支持,能在精度和速度间取得最佳平衡。使用float64不仅会浪费一倍内存和显存,训练速度也可能更慢,而精度提升对最终模型效果往往微乎其微。

# 推荐 weights = np.zeros((256, 128), dtype=np.float32) # 不推荐(除非有特殊精度要求) weights = np.zeros((256, 128), dtype=np.float64)

场景三:逻辑掩码(Mask)创建布尔掩码数组时,务必使用dtype=bool。虽然用dtype=int且值为0/1也能工作,但bool类型在内存(每个元素1字节)和语义清晰度上都有优势。

mask_bool = np.zeros((10, 10), dtype=bool) # 清晰,高效 mask_int = np.zeros((10, 10), dtype=int) # 可以,但不推荐用于纯掩码

注意np.zeros返回的数组,其元素是真正的“零值”,对应于该数据类型的零。对于float0.0,对于int0,对于boolFalse,对于complex0j,对于objectNone。这确保了数组在算术运算中的行为符合预期。

2.3order参数对后续操作的实际影响

让我们通过一个简单的例子感受order的差异:

import numpy as np # 创建C风格和F风格的3x4零数组 arr_c = np.zeros((3, 4), order='C') arr_f = np.zeros((3, 4), order='F') print("C-order array is C-contiguous:", arr_c.flags['C_CONTIGUOUS']) # True print("C-order array is F-contiguous:", arr_c.flags['F_CONTIGUOUS']) # False print("F-order array is C-contiguous:", arr_f.flags['C_CONTIGUOUS']) # False print("F-order array is F-contiguous:", arr_f.flags['F_CONTIGUOUS']) # True # 扁平化(flatten/ravel)操作:C顺序返回行优先展开,F顺序返回列优先展开 print("arr_c.flatten():", arr_c.flatten()) # 按行展开 [0., 0., 0., 0., 0., ...] print("arr_f.flatten():", arr_f.flatten()) # 按列展开 [0., 0., 0., 0., 0., ...] (顺序相同,因为都是0) # 但对于非零数组,差异就明显了 arr_c_num = np.arange(12).reshape(3,4) arr_f_num = np.arange(12).reshape(3,4, order='F') print("C-order reshape flatten:", arr_c_num.flatten()) # [ 0 1 2 3 4 5 6 7 8 9 10 11] print("F-order reshape flatten:", arr_f_num.flatten()) # [ 0 4 8 1 5 9 2 6 10 3 7 11]

当你进行涉及连续内存访问的操作(如向量化计算、与C/Fortran代码交互)时,使用匹配的order能避免不必要的内存拷贝,从而提升性能。

3. 核心登场:np.zeros_like的模仿艺术

有了对np.zeros的深刻理解,np.zeros_like就变得非常直观了。它的目标就是自动从原型数组中提取shape,dtype,order等信息,然后调用np.zeros

3.1 函数签名与参数解析

numpy.zeros_like(a, dtype=None, order='K', subok=True, shape=None)

  • a: 输入数组,即被模仿的原型。这是唯一必需的参数。
  • dtype: 覆盖数据类型。这是一个关键且容易混淆的参数。如果为None(默认),则新数组使用a.dtype。如果指定了dtype,则新数组使用指定的dtype但形状等其他属性依然模仿a。这允许你创建一个形状、布局和a一样,但元素类型不同的零数组。
  • order: 覆盖内存布局。默认是'K'(keep),意味着尽可能保持输入数组a的内存顺序。其他选项'C','F','A','K'的含义如下:
    • 'C': 强制C顺序。
    • 'F': 强制F顺序。
    • 'A': 如果a是F连续的,则输出为F顺序,否则为C顺序。
    • 'K': 尽可能匹配a的内存布局(这是最“像”的模式)。
  • subok: 布尔值,默认为True。如果为True,则返回的数组与a具有相同的子类型(例如,如果anp.matrix,则返回matrix)。如果为False,则返回一个基础的ndarray。由于np.matrix即将被弃用,通常保持默认True即可,除非你有特殊需求。
  • shape: 覆盖形状。这是一个不常用的参数。如果指定,则新数组使用此形状,但dtypeorder等依然从a推断或由参数指定。这打破了“完全像”的规则,仅在特定覆盖场景有用。

3.2 经典使用场景与代码示例

场景一:为已有数组创建同结构的零值容器这是最普遍的用法,用于存储计算结果。

import numpy as np # 假设我们有一个从文件加载的复杂数据数组 original_data = np.random.randn(100, 50).astype(np.float32) # 形状(100,50), float32类型 # 我们需要一个同样大小的数组来存储处理后的结果 result_container = np.zeros_like(original_data) # 现在 result_container 是 (100, 50) 的 float32 零数组 print(result_container.shape, result_container.dtype) # (100, 50) float32

场景二:保持内存布局,用于优化计算

# 创建一个列优先的数组(例如,从某些列运算优化的代码中得来) col_major_array = np.array([[1, 2, 3], [4, 5, 6]], order='F') print(col_major_array.flags['F_CONTIGUOUS']) # True # 使用 zeros_like 创建同布局的零数组,后续列操作效率高 zero_col_major = np.zeros_like(col_major_array) # 默认 order='K', 保持F连续 print(zero_col_major.flags['F_CONTIGUOUS']) # True # 如果错误地使用 np.zeros(col_major_array.shape), 会得到默认的C顺序数组 wrong_zeros = np.zeros(col_major_array.shape) print(wrong_zeros.flags['C_CONTIGUOUS']) # True # 后续将 col_major_array 与 wrong_zeros 进行按列运算,可能触发不必要的内存拷贝

场景三:处理结构化数组(Structured Arrays)这是zeros_like真正闪耀的地方,手动处理会非常麻烦。

# 定义一个描述人员信息的结构化dtype dtype = np.dtype([('name', 'U10'), ('age', 'i4'), ('height', 'f8')]) # 创建一个示例数组 people = np.array([('Alice', 30, 165.5), ('Bob', 25, 180.0)], dtype=dtype) # 使用 zeros_like 轻松创建同结构的零值数组 empty_people = np.zeros_like(people) print(empty_people) # 输出: [('', 0, 0.) ('', 0, 0.)] # 注意:字符串字段被初始化为空字符串 '',整数为0,浮点为0.0 # 如果手动创建,你需要精确复制复杂的dtype定义:np.zeros(2, dtype=dtype)

场景四:覆盖dtype,进行类型转换式初始化有时我们需要一个和输入数组形状相同,但类型不同的零数组。

# 有一个整数索引数组 indices = np.array([1, 5, 10, 20], dtype=np.int32) # 我们需要一个同样大小的浮点数组来存储计算出的分数 scores = np.zeros_like(indices, dtype=np.float64) # 形状模仿indices,但类型改为float64 print(scores.dtype) # float64 print(scores.shape) # (4,)

3.3order='K'(keep) 的微妙之处

order='K'zeros_like的默认行为,也是它“模仿”精神的精髓。它不仅仅是检查数组是C连续还是F连续,还会考虑更复杂的内存布局。

# 创建一个非连续视图(切片) arr = np.arange(12).reshape(3, 4) # C连续 sub_arr = arr[:, ::2] # 取所有行,第0和第2列。这是一个非连续视图。 print(sub_arr.flags['C_CONTIGUOUS']) # False print(sub_arr.flags['F_CONTIGUOUS']) # False # 使用 zeros_like 默认 (order='K') new_arr_k = np.zeros_like(sub_arr) # new_arr_k 会是什么顺序?它可能既不是C连续也不是F连续。 # 它创建了一个新的、独立的内存块,但其步长(strides)可能被设计为与sub_arr的“逻辑布局”最匹配, # 而不一定是物理上连续的。这保证了后续对new_arr_k进行类似sub_arr的索引操作时,有高效的内存访问模式。 # 对比 order='C' new_arr_c = np.zeros_like(sub_arr, order='C') print(new_arr_c.flags['C_CONTIGUOUS']) # True, 但内存布局与sub_arr的“逻辑”可能不匹配

对于高级用户,理解order='K'有助于写出更高效、更通用的代码。对于大多数日常应用,知道它“尽可能保持原样”就足够了。

4. 陷阱、误区与最佳实践

即使是最简单的函数,也有坑等着你。下面是我在多年使用中总结的一些经验教训。

4.1 性能陷阱:对大数组的重复调用

在循环或频繁调用的函数内部使用zeros_like(或zeros)创建大型数组,是常见的性能瓶颈。每次调用都会分配新的内存并初始化为零,这是一个相对昂贵的操作。

反面教材

def process_data_bad(data_chunks): results = [] for chunk in data_chunks: # 每次迭代都分配一个新的零数组 temp = np.zeros_like(chunk) # ... 一些计算 ... results.append(temp) return results

优化方案: 如果可能,预分配一个足够大的结果数组,然后在循环中填充相应的切片。

def process_data_good(data_chunks): # 假设我们知道总大小,或者可以计算出来 total_size = sum(ch.size for ch in data_chunks) # 根据第一个chunk推断dtype和可能的内存布局 dtype = data_chunks[0].dtype # 预分配一个大数组(这里简化为一维,实际情况可能更复杂) final_result = np.zeros(total_size, dtype=dtype) offset = 0 for chunk in data_chunks: length = chunk.size # 直接操作预分配数组的切片,无需每次新建 final_result[offset:offset+length] = chunk * 2 # 示例计算 offset += length return final_result

如果无法预分配总大小,至少可以考虑在循环外复用同一个临时数组(如果大小不变)。

4.2dtype覆盖的语义混淆

zeros_like(a, dtype=some_dtype)的行为是:形状等像a,但类型像some_dtype。这有时会和zeros(a.shape, dtype=some_dtype)混淆。关键在于,zeros_like还会考虑ordersubok属性。

a = np.array([[1,2],[3,4]], order='F', dtype=np.int16) # 目标:创建一个float64的零数组,但希望保持F顺序 # 方法1:使用 zeros, 必须手动指定order z1 = np.zeros(a.shape, dtype=np.float64, order='F') print(z1.flags['F_CONTIGUOUS']) # True # 方法2:使用 zeros_like, 更简洁,dtype参数覆盖了原类型 z2 = np.zeros_like(a, dtype=np.float64) # 默认order='K', 会保持F顺序 print(z2.flags['F_CONTIGUOUS']) # True print(z2.dtype) # float64

在这个例子中,zeros_like的写法更安全,因为你不需要记住aorder='F'

4.3 与np.empty_likenp.ones_like的对比选择

NumPy 提供了*_like函数家族:

  • np.zeros_like: 分配内存并初始化为0。
  • np.ones_like: 分配内存并初始化为1。
  • np.empty_like: 分配内存但不进行初始化,内容为内存中的任意值(垃圾值)。

empty_like的使用场景与风险empty_like是三者中最快的,因为它跳过了初始化步骤。但它极其危险,因为未初始化的数组包含不可预测的值,直接使用会导致程序行为不确定和难以调试的bug。

安全使用empty_like的唯一模式是:立即用确定的数据覆盖整个数组。

# 安全用法:立即全覆盖 result = np.empty_like(original_data) np.copyto(result, original_data) # 或者 result[:] = original_data # 或者 result = np.empty_like(original_data) result = original_data * 2 + 1 # 整个数组被重新赋值覆盖 # 危险用法:部分操作 danger = np.empty_like(original_data) danger[original_data > 0] = 1 # 错误!那些 original_data <= 0 的位置仍然是垃圾值!

经验法则: 除非你百分之百确定在接下来的代码中,新数组的每一个元素都会被显式赋值,否则永远使用zeros_likeones_like。性能的微小提升远不及程序正确性重要。在绝大多数应用中,初始化的开销可以忽略不计。

4.4 处理特殊数组类型(如矩阵np.matrix

虽然np.matrix已被弃用,但在旧代码中仍可能遇到。zeros_likesubok参数在这里起作用。

import numpy as np # 注意:np.matrix 即将弃用,此处仅为演示 m = np.matrix([[1, 2], [3, 4]]) print(type(m)) # <class 'numpy.matrix'> z_subok_true = np.zeros_like(m) # subok=True 是默认值 print(type(z_subok_true)) # <class 'numpy.matrix'>, 保持了矩阵类型 z_subok_false = np.zeros_like(m, subok=False) print(type(z_subok_false)) # <class 'numpy.ndarray'>, 降级为普通ndarray

对于新代码,建议直接使用ndarray并通过@运算符进行矩阵乘法,避免使用np.matrix

5. 实战进阶:在复杂项目中应用zeros_like

让我们看几个更综合的例子,看看zeros_like如何融入实际的算法或数据处理流程中。

5.1 实现一个简单的梯度下降算法

在机器学习中,梯度下降需要初始化与模型参数同样形状的梯度累加器或动量项。

def gradient_descent(X, y, learning_rate=0.01, epochs=1000): """ 简单的线性回归梯度下降 X: 特征矩阵 (m_samples, n_features) y: 目标向量 (m_samples,) """ m, n = X.shape # 初始化参数 theta, 形状为 (n_features,) theta = np.random.randn(n) # 初始化一个与 theta 结构一致的零数组,用于存储梯度 # 这里用 zeros_like 确保了维度一致,代码意图清晰 gradient = np.zeros_like(theta) for epoch in range(epochs): # 1. 计算预测值 predictions = X.dot(theta) # 2. 计算误差 errors = predictions - y # 3. 计算梯度 (这里使用循环是为了清晰,实际应用应向量化) # 但为了演示 zeros_like 的用途,我们显式地用一个零数组来累加 gradient.fill(0) # 将已有的 gradient 数组清零,准备新一轮累加 for j in range(n): gradient[j] = (1/m) * np.sum(errors * X[:, j]) # 4. 更新参数 theta -= learning_rate * gradient return theta

在这个例子中,gradient = np.zeros_like(theta)gradient = np.zeros(n)更好,因为它明确表达了“梯度向量与参数向量同形”这一概念,即使未来theta的初始化方式或维度发生变化,这行代码也无需修改。

5.2 图像处理中的掩码(Mask)创建

在计算机视觉中,我们经常需要根据某个条件创建一个与原始图像同尺寸的布尔掩码。

import cv2 import numpy as np # 假设我们读入一张彩色图像 image = cv2.imread('example.jpg') # 形状为 (H, W, 3), dtype=uint8 height, width, channels = image.shape # 场景:创建一个掩码,标记出亮度较高的区域 # 先将图像转为灰度图 gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 设定一个阈值 threshold = 200 # 创建一个与灰度图(也是与原图空间尺寸一致)同形状的布尔掩码 # 使用 zeros_like 并指定 dtype=bool bright_mask = np.zeros_like(gray, dtype=bool) bright_mask[gray > threshold] = True # 现在 bright_mask 是一个 (H, W) 的布尔数组,True代表高亮区域 # 我们可以利用这个掩码对原图进行操作,例如将高亮区域涂红 image_masked = image.copy() # 这里利用了广播机制:image_masked[bright_mask] 会选择所有通道上对应True位置的像素 image_masked[bright_mask] = [0, 0, 255] # BGR格式下的红色 # 另一个常见操作:创建一个与原图同尺寸的纯色画布 # 例如,创建一个黑色的画布,用于绘制轮廓 black_canvas = np.zeros_like(image) # 自动继承 uint8 类型和 (H,W,3) 形状 # 或者创建一个白色的画布 white_canvas = np.ones_like(image) * 255

zeros_like(gray, dtype=bool)在这里一举两得:一是获得了正确的空间维度(H, W),二是得到了我们需要的布尔类型,避免了先创建uint8数组再转换的冗余步骤。

5.3 自定义函数中的通用输出分配

当你编写一个要处理各种输入数组的库函数时,zeros_like是分配输出数组的瑞士军刀。

def normalize_along_axis(arr, axis=0): """ 沿指定轴对数组进行归一化(减去均值,除以标准差) 返回一个与输入 arr 形状、dtype 相同的数组。 """ # 参数校验等略... # 使用 zeros_like 创建输出数组,确保与输入完全兼容 result = np.zeros_like(arr) # 计算均值和标准差,keepdims=True 保持维度以便广播 mean = np.mean(arr, axis=axis, keepdims=True) std = np.std(arr, axis=axis, keepdims=True) # 防止除零 std = np.where(std == 0, 1.0, std) # 广播计算 result = (arr - mean) / std return result # 测试 data_2d = np.random.randn(5, 3).astype(np.float32) normed_data = normalize_along_axis(data_2d, axis=0) print(normed_data.shape == data_2d.shape) # True print(normed_data.dtype == data_2d.dtype) # True data_3d = np.random.randn(4, 5, 6).astype(np.float64) normed_data_3d = normalize_along_axis(data_3d, axis=2) print(normed_data_3d.shape == data_3d.shape) # True print(normed_data_3d.dtype == data_3d.dtype) # True

这个normalize_along_axis函数可以接受任何形状、任何数值dtypefloat16,float32,float64)的数组,并返回一个与之完全匹配的结果。zeros_like在这里保证了输出的“外壳”与输入一致,而内部的计算由广播机制高效完成。这种模式在科学计算库中非常普遍。

np.zerosnp.zeros_like远不止是创建零数组的工具,它们是构建可预测、可维护、高性能数值计算代码的基石。理解dtypeorder的细微差别,能帮助你在内存和速度上做出最优选择。而zeros_like的“模仿”特性,则是编写通用、健壮代码的关键。记住,在不确定或需要绝对安全时,优先使用zeros_like;在追求极限性能且能保证安全覆盖时,才考虑empty_like。将这些函数运用得当,你的 NumPy 代码会变得更加清晰和强大。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/17 20:44:39

2026年牵引与工业配送AMR品牌对比推荐:拖拽料车和货架怎么选

工厂所说的“搬运”可能是背负料箱、牵引现有料车、潜入货架顶升&#xff0c;或与辊筒线自动交接。选型应先锁定载具和交接方式&#xff0c;再核对正式载荷、总长、转弯、重心、坡道、安全区、接口和异常恢复。只按最大载重买车&#xff0c;往往会在挂接、落架或满载转弯时失败…

作者头像 李华
网站建设 2026/8/17 20:41:43

旋转矩阵:二维点绕原点旋转

1. 引言 在计算机图形学、物理模拟和数学可视化中&#xff0c;点的旋转是一个基础且重要的概念。本文将使用 Python 的 Matplotlib 库&#xff0c;通过动画直观地演示一个二维点如何绕原点进行旋转。我们将从旋转矩阵的数学原理出发&#xff0c;逐步编写代码&#xff0c;最终生…

作者头像 李华
网站建设 2026/8/17 20:41:40

JDBC深度解析:从核心架构到生产环境实战优化

1. 项目概述&#xff1a;为什么今天还要深挖JDBC&#xff1f; 如果你在Java开发这条路上走了有些年头&#xff0c;可能会觉得JDBC是个“老古董”——Spring Data JPA、MyBatis Plus这些框架用起来多香&#xff0c;谁还愿意去写那些冗长的 Connection 、 Statement 和 Resu…

作者头像 李华
网站建设 2026/8/17 20:39:56

从三六零零元转让哪吒汽车股权看对赌协议与商业博弈

1. 从“零元”交易看商业世界的非典型操作最近&#xff0c;一则关于三六零公司“零元”转让其持有的哪吒汽车部分股权的新闻&#xff0c;在商业和创投圈里激起了不小的水花。很多人第一眼看到“零元”、“甩卖”、“违约”这些字眼&#xff0c;直觉反应就是&#xff1a;这肯定是…

作者头像 李华
网站建设 2026/8/17 20:37:45

PowerBI主题模板完整实战指南:3步用JSON统一报表全部图表样式

PowerBI主题模板完整实战指南&#xff1a;3步用JSON统一报表全部图表样式 【免费下载链接】PowerBI-ThemeTemplates Snippets for assembling Power BI Themes 项目地址: https://gitcode.com/gh_mirrors/po/PowerBI-ThemeTemplates 你算过吗&#xff1f;做一份 Power B…

作者头像 李华
网站建设 2026/8/17 20:37:11

SteamAutoCrack完整上手教程:三步让Steam游戏离线也能玩

SteamAutoCrack完整上手教程&#xff1a;三步让Steam游戏离线也能玩 【免费下载链接】Steam-auto-crack Steam Game Automatic Cracker 项目地址: https://gitcode.com/gh_mirrors/st/Steam-auto-crack 前阵子出差&#xff0c;高铁上打开笔记本想玩刚买的两款游戏&#…

作者头像 李华