You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python测试Mock x对象遇TypeError:shape为函数而非元组

问题排查与解决:TypeError: can only concatenate tuple (not "function") to tuple

原类方法代码

def augment_data(
    self,
    number_of_images_tobe_gen: int = SETTING.AUGMENTATION_SETTING.NUMBER_OF_IMAGES_TOBE_GENERATED,
    augment_data_address: Path = SETTING.AUGMENTATION_SETTING.AUGMENTED_IMAGES_DIR_ADDRESS,
):
    """
    This function augments the images and save them into the dataset_augmented folder.
    :param number_of_images_tobe_gen: number of images to be generated
    :param augment_data_address: address to save the augmented images
    :return: None
    """
    Augment_data_gen = image.ImageDataGenerator(
        rotation_range=SETTING.AUGMENTATION_SETTING.ROTATION_RANGE,
        width_shift_range=SETTING.AUGMENTATION_SETTING.WIDTH_SHIFT_RANGE,
        height_shift_range=SETTING.AUGMENTATION_SETTING.HEIGHT_SHIFT_RANGE,
        shear_range=SETTING.AUGMENTATION_SETTING.SHEAR_RANGE,
        zoom_range=SETTING.AUGMENTATION_SETTING.ZOOM_RANGE,
        horizontal_flip=SETTING.AUGMENTATION_SETTING.HORIZONTAL_FLIP,
        fill_mode=SETTING.AUGMENTATION_SETTING.FILL_MODE,
    )

    main_address = (
        augment_data_address
        or SETTING.AUGMENTATION_SETTING.AUGMENTED_IMAGES_DIR_ADDRESS
    )

    for image_category in self.image_dict.keys():
        # check if a dir dataset_augmented exists if not create it
        if not os.path.exists(main_address / image_category):
            os.makedirs(main_address / image_category)
        # check if the dir is empty if not delete all the files
        else:
            # empty the previous augmented images
            for file in (main_address / image_category).iterdir():
                os.remove(file)

        number_of_images = self.image_dict[image_category]["number_of_images"]
        for _ in tqdm(
            range(0, number_of_images_tobe_gen),
            desc=f"Augmenting {image_category} images:",
        ):
            # generate a random number integer between 0 and number_of_images
            rand_img_num = int(random.random() * number_of_images)

            img_address = self.image_dict[image_category]["image_list"][
                rand_img_num
            ]
            for case in SETTING.IGNORE_SETTING.IGNORE_LIST:
                if case == img_address.name:
                    logger.debug(f"Found {case} in {image_category} folder")
                    continue
            # check if the image name is in the ignore list, if so continue
            if img_address in SETTING.IGNORE_SETTING.IGNORE_LIST:
                continue

            logger.debug(f"Image address: {img_address}")

            img = load_img(img_address)

            x = img_to_array(img)
            x = x.reshape((1,) + x.shape)

            for _ in Augment_data_gen.flow(
                x,
                batch_size=SETTING.AUGMENTATION_SETTING.BATCH_SIZE,
                save_to_dir=main_address / image_category
                or SETTING.AUGMENTATION_SETTING.AUGMENTED_IMAGES_DIR_ADDRESS
                / image_category,
                save_prefix=SETTING.AUGMENTATION_SETTING.AUGMENTED_IMAGES_SAVE_PREFIX,
                save_format=SETTING.AUGMENTATION_SETTING.AUGMENTED_IMAGES_SAVE_FORMAT,
            ):
                break

单元测试代码

class XObjClass:
    def shape(self):
        return ()

    def reshape(self, *args, **kwargs):
        print(args, kwargs)
        return self


def img_to_array_func(*args, **kwargs):
    print(args, kwargs)
    return XObjClass


def load_image_func(*args, **kwargs):
    print(args, kwargs)
    return None


class ImageAddressObject:
    @property
    def name(self):
        return "test_image.jpg"


def image_dict_object(*args, **kwargs):
    print(args, kwargs)
    return {
        "pdc_bit": {"image_list": [ImageAddressObject], "number_of_images": 0},
        "rollercone_bit": {"image_list": [ImageAddressObject], "number_of_images": 0},
    }


def test_augment_data(mocker, _object):

    # mocker patch the property categories_name
    mocker.patch(
        "neural_network_model.process_data.Preprocessing.categorie_name",
        new_callable=mocker.PropertyMock,
        return_value=["pdc_bit", "rollercone_bit"],
    )

    # mocker patch the image dict object
    mocker.patch(
        "neural_network_model.process_data.Preprocessing.image_dict",
        new_callable=mocker.PropertyMock,
        side_effect=image_dict_object,
    )

    # mocker patch load_image function
    mocker.patch(
        "neural_network_model.process_data.load_img",
        side_effect=load_image_func,
    )

    # mocker patch the img_to_array function
    mocker.patch(
        "neural_network_model.process_data.img_to_array",
        side_effect=img_to_array_func,
    )

    _object.augment_data(number_of_images_tobe_gen=5)

错误信息

x = img_to_array(img)
>               x = x.reshape((1,) + x.shape)
E               TypeError: can only concatenate tuple (not "function") to tuple

../neural_network_model/process_data.py:221: TypeError

问题原因

  1. img_to_array_func返回类而非实例:原代码中img_to_array返回numpy数组实例,测试代码里却直接返回XObjClass类,导致x是类对象,x.shape指向类的方法而非属性值。
  2. shape定义不符合原代码逻辑:原代码中x.shape是属性(numpy数组的特性),但测试里把shape定义成了方法,即使是实例,访问x.shape也会得到函数对象。
  3. 额外索引隐患:image_dict中number_of_images设为0,会导致rand_img_num计算结果为0,而image_list里存的是类而非实例,后续访问img_address.name也会出错。

解决方法

修改后的测试代码如下:

class XObjClass:
    @property
    def shape(self):
        return ()  # 将shape改为属性,匹配numpy数组的行为

    def reshape(self, *args, **kwargs):
        print(args, kwargs)
        return self


def img_to_array_func(*args, **kwargs):
    print(args, kwargs)
    return XObjClass()  # 返回类的实例,而非类本身


def load_image_func(*args, **kwargs):
    print(args, kwargs)
    return None


class ImageAddressObject:
    @property
    def name(self):
        return "test_image.jpg"


def image_dict_object(*args, **kwargs):
    print(args, kwargs)
    return {
        "pdc_bit": {"image_list": [ImageAddressObject()], "number_of_images": 1},  # 存入实例,同时修正number_of_images避免索引问题
        "rollercone_bit": {"image_list": [ImageAddressObject()], "number_of_images": 1},
    }


def test_augment_data(mocker, _object):

    # mocker patch the property categories_name
    mocker.patch(
        "neural_network_model.process_data.Preprocessing.categorie_name",
        new_callable=mocker.PropertyMock,
        return_value=["pdc_bit", "rollercone_bit"],
    )

    # mocker patch the image dict object
    mocker.patch(
        "neural_network_model.process_data.Preprocessing.image_dict",
        new_callable=mocker.PropertyMock,
        side_effect=image_dict_object,
    )

    # mocker patch load_image function
    mocker.patch(
        "neural_network_model.process_data.load_img",
        side_effect=load_image_func,
    )

    # mocker patch the img_to_array function
    mocker.patch(
        "neural_network_model.process_data.img_to_array",
        side_effect=img_to_array_func,
    )

    _object.augment_data(number_of_images_tobe_gen=5)

修改说明

  • 给XObjClass的shape加上@property装饰器,让它变成属性,匹配原代码中numpy数组的shape访问方式。
  • img_to_array_func返回XObjClass()实例,确保x是实例对象,能正确调用reshape方法和访问shape属性。
  • image_dict中image_list存入ImageAddressObject实例,同时把number_of_images设为1,避免随机索引计算错误和属性访问错误。

内容的提问来源于stack exchange,提问作者Amin Tash

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 12:47:03