클래스에 대해서 알아보자

클래스에 대해서 알아보자

Class는 객체지향 프로그래밍에서 가장 중요하고도 까다롭다.
흔히 말하는 상속이 무엇인지, 어떤 상황에서 상속을 하는지, 상속을 할 수 없을 때는
객체 관계를 어떻게 표현하는지 알아보자.

클래스 관계

클래스 관계를 나타내는 방법으로 IS-A와 HAS-A가 있다.

1.1 IS-A : 상속

IS-A는 ‘은 ~의 한 종류다’를 말한다. 노트북과 컴퓨터를 예를 들어보자. 노트북은 컴퓨터의 한 종류일까?
그렇다. 이런 관계일 경우 Computer와 laptop 클래스는 IS-A관계라고 말할 수 있다.
IS-A관계 인지 아닌지 분간이 안된다면, ‘한 종류다’라는 의미가 있는지 생각해 보자.

이런 IS-A관계를 프로그램에서 표현할 때는 상속을 사용한다. 상속은 IS-A관계에서 설계가 쉽다.

상속을 하는 클래스와 상속을 받는 클래스를 나눠 볼 수 있는데 표현은 다음과 같다.

상속을 하는 클래스

  • 기본 클래스
  • 부모 클래스
  • 슈퍼 클래스

상속을 받는 클래스

  • 파생 클래스
  • 자식 클래스
  • 서브 클래스

코드로 laptop과 computer 클래스를 설계해 보자.

1
2
3
4
5
6
7
8
9
10
class Computer:
def __init__(self, cpu, ram):
self.CPU = cpu
self.RAM = ram

def browse(self):
print('browse')

def work(self):
print('work')

이 코드에서 인스턴스 멤버는 CPU와 RAM이다. 인스턴스 메서드는 browse()와 일을 하는 work()이다.
노트북은 컴퓨터의 모든 멤버와 메서드를 가진다. 노트북에도 CPU와 RAM이 있고, 같은 일을 하기 때문이다.
어떤 객체가 다른 객체의 모든 특성과 기능을 가진 상태에서 그 외에 다른 특성이나 기능을 가지고 있다면 상속해서 쓰는게 편하다.

노트북 클래스를 설계해보자

1
2
3
4
5
6
7
class Laptop(Computer):
def __init__(self, cpu, ram, battery):
super().__init__(cpu, ram)
self.battery = battery

def move(self, to):
print('move to {}'.format(to))

노트북의 클래스 옆에 Computer가 붙은게 보인다. 이는 컴퓨터 클래스를 상속하겠다는 뜻이다.
이렇게 되면 노트북은 컴퓨터 클래스가 가진 모든 멤버와 메서드를 가지게 된다. 노트북도 browse()와
work()가 가능하다는 말이다. 확실히 손이 덜 피곤하다는 게 느껴질 것이다.

super는 무엇일까? 이것은 기본 클래스를 의미한다. 기본 클래스는 위에서 써놨듯이, 상속을 하는 클래스, 즉
컴퓨터 클래스를 가리킨다. CPU와 RAM은 기본 클래스의 생성자를 이용해 초기화가 되었기 때문에 남은 한 멤버인 battery만 할당해 주면 된다.

그리고 노트북에만 있는 move메서드를 입력해준다. 이렇게 되면, 노트북만의 메서드를 하나 갖게 된다.

테스트 코드는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
if __name__ == "__main__":
lap = Laptop('intel', 16, 'powerful')
lap.browse()
lap.work()
lap.move('office')
````

### 1.2 HAS-A : 합성 또는 통합
HAS-A관계는 '~이 ~을 가진다 혹은 포함한다'를 의미한다. Computer는 CPU와 RAM을 가지는데,
여기서 이 관계를 HAS-A관계라고 부를 수 있다.

경찰과 총의 관계를 생각해보자. 경찰은 총을 가지고 있다. 경찰과 총은 HAS-A관계가 성립한다.
주의해야할 점이 있는데, HAS-A관계에는 합성과 통합이라는 표현방법이 존재한다.

컴퓨터와 CPU의 관계를 합성으로 표현하고, 경찰과 총의 관계를 통합으로 표현해보자.
``` python
class CPU :
pass

class RAM :
pass

class Computer :
def __init__(self):
self.cpu = CPU()
self.ram = RAM()

Computer는 인스턴스 멤버 cpu를 가진다. 생성자에서 CPU 객체를 생성해서 멤버 cpu에게 할당한다.
이렇게 되면 Computer라는 객체가 생성이 될 때, CPU와 RAM이 같이 생성이 되고, 사라질때 같이 사라지게 된다.

이 둘의 관계는 매우 강한 관계를 맺고 있다고 할 수 있다. 이런 관계를 합성이라고 한다.

경찰과 총의 관계를 살펴보자

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Gun :
def __init__(self, kind):
self.kind = kind

def bang(self):
print('bang bang')

class Police :
def __init__(self):
self.gun = None

def acquire_gun(self, gun):
self.gun = gun

def release_gun(self):
gun = self.gun
self.gun = None
return gun

def shoot(self):
if self.gun:
self.gun.bang()
else :
print("Unable to shoot")

이 관계에서는 Police 객체가 만들어질 때 Gun 객체를 가지고 있지 않다. 이후 acquire_gun()메서드를
통해서 Gun 객체를 멤버로 가지게 된다. 이 관계 역시 HAS-A이다. 또한 release_gun()으로 가진 총을
반납할 수도 있다. 이 두 메서드를 이용해 총을 가진 경찰, 총이 없는 경찰 모두를 표현할 수 있다.

하지만 컴퓨터 클래스와 다른 점은, 경찰은 언제든지 Gun을 가질 수 있고 해제할 수 있다는 점이다.
관계가 컴퓨터에 비해 훨씬 약하다는 느낌이 들 것이다. 이런 약한 관계를 통합이라고 부른다.

2. 메서드 오버라이딩과 다형성(Polymorphism)

OOP에서 가장 중요한 개념은 다형성이다(polymorphism). 나는 이 ‘폴리몰피즘’에 대해 노이로제가 걸렸었던 적이 있다.
고려대에서 진행한 Bigdata X Campus 교육에서였다. 파이썬 강의를 들으면서 강사는 “뽈리몰피즘! 뽈리몰피즘이 중요하죠!” 라고 열변을 토했고, 매 강의마다 항상 강조되었었다.
‘도대체 polymorphism이 뭐길래’ 라는 생각이 들었었고, 이 책을 보면서 그 갈증이 어느정도 해결이 되었다.

다형성이란 ‘상속 관계에 있는 다양한 클래스의 객체에서 같은 이름의 메서드를 호출할 때, 각 객체가 서로 다르게 구현된 메서드를 호출함으로써 서로 다른 행동, 기능, 결과를 가져오는 것’을 의미한다. 이를 구현하기 위해서는 파생 클래스 안에서 상속받은 메서드를 다시 구현하는 메서드 오버라이딩이라고 부른다.

2.1 메서드 오버라이딩

먼저 코드를 살펴보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CarOwner:
def __init__(self, name):
self.name = name

def concentrate(self):
print('{} can not do anything else'.format(self.name))


class Car:
def __init__(self, owner_name):
self.owner = CarOwner(owner_name)

def drive(self):
self.owner.concentrate()
print('{} is driving now.'.format(self.owner.name))

drive()메서드를 보면 Car 객체는 반드시 차 주인인 CarOwner객체가 운정해야 하고 차 주인은 운전에만
집중해야 한다. drive()메서드가 나오자마자 CarOwner객체의 concentrate()메서드를 호출해서 차 주인이 운전외에는 아무것도 못하게 한다.

이번에는 자율주행차를 만들어보자

1
2
3
class SelfDrivingCar(Car):
def drive(self):
print('Car is driving by itself')

자율주행차에는 상속받은 drive가 어울리지 않는다. 새롭게 바꿔줄 필요가 있다. drive()메서드를 제외하고는 나머지 멤버와 메서드는 그대로 사용한다. 이러한 경우에는 drive()메서드만 클래스 안에서 재정의해준다.

이렇게 클래스 안에서, 맘에 들지 않는 메서드를 재정의 하는 것을 메서드 오버라이딩이라고 한다.
자율주행차의 차 주인은 더 이상 운전에 집중하지 않아도 된다. 따라서 오버라이딩된 drive()메서드에서는
concentrate()를 호출하지 않는다.

여기서 정리해보자면, drive()메서드는 같은 이름이지만 객체에 따라 다른 기능을 하게 된다. 이처럼
같은 이름의 메서드를 호출해도 호출한 객체에 따라 다른 결과를 내는 것을 다형성이라고 한다.

2.2 다형성

다형성에 대해 좀 더 깊이 알아보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Animal:
def eat(self):
print('eat something')


class Lion(Animal):
def eat(self):
print('eat meat')


class Cow(Animal):
def eat(self):
print('eat grass')


class Human(Animal):
def eat(self):
print('eat meat and grass')


if __name__ == "__main__":
animals = []
animals.append(Lion())
animals.append(Cow())
animals.append(Human())

for animal in animals:
animal.eat()

이 코드의 Animal 클래스에는 eat()메서드가 있다. 모든 동물은 반드시 먹어야 한다는 가정이다.
하지만 동물마다 먹는 종류는 다르기 때문에 육식 동물의 대표로 사자를 설정했고, 초식 동물의 대표로 소를
설정했다. 그리고 잡식 동물로 사람을 설정했다. 나는 소고기를 쌈싸먹는 것을 좋아한다.

코드의 반복문의 마지막 부분에서 animal.eat()은 다형성을 구현한 부분이다. animals 리스트에서 객체를 하나씩 불러와 eat()메서드를 호출할 때, 메서드를 호출한 쪽에서는 육식동물인지 초식동물인지 잡식인지 고민할 필요가 없다. 각 객체는 오버라이딩된 메서드를 호출하기 때문이다.

그렇게 되면 여기서는 그냥 무엇인가를 먹는 동물은 없다. 그러니까 Animal은 eat something하는 게 있는데 사용하는 동물이 아무도 없다. 안써버리자니 문제가 되고, 뭔가 낭비같다.

이럴 때는 Animal 클래스를 추상 클래스로 만들면 된다.

추상 클래스는 독자적으로 인스턴스를 만들 수 없고 함수의 몸체가없는 추상 메서드를 하나 이상 가지고 있어야 한다. 또한 이 클래스를 상속받는 파생 클래스는 추상 메서드를 반드시 오버라이딩 해야한다. 당연히 아무것도 없으니까!

Animal을 추상 클래스로 변경해보자.

1
2
3
4
5
6
7
from abc import *

class Animal(metaclass = ABCMeta):
@abstractmethod
def eat(self):
pass
...

먼저 abc모듈을 가져온다.(abstract base class) 그 후 @abstractmehod 데코레이터를 붙여준다.
여기서 메서드 구현하는 부분을 pass로 비워두면 eat()은 추상 메서드가 된다.
이제 Animal을 상속받는 모든 파생 클래스는 eat()을 오버라이딩 해야한다.

클래스 설계 예제

클래스를 설계할 때느느 다음 두가지를 고려해야 한다.

  • 공통 부분을 기본 클래스로 묶는다.
  • 부모가 추상클래스인 경우를 제외하고, 파생 클래스에서 기본 클래스의 여러 메서드를 오버라이딩한다면 파생 클래스는 만들지 않는 것이 좋다.

Character 클래스 만들기

게임 캐릭터를 만들어보면서 클래스를 정리해보자.
게임에 등장하는 캐릭터는 플레이어 우리 자신과 몬스터이다.
모든 캐릭터(추상 클래스)는 다음과 같은 특성을 지닌다.

  • 인스턴스 멤버 : 이름, 체력, 공격력을 가진다.
  • 인스턴스 메서드 : 공격, 공격당했을 때는 피해를 입는다.(모두 추상 메서드로 구현한다.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from abc import *

class Character(metaclass = ABDMeta):
def __init__(self, name, hp, power):
self.name = name
self.hp = hp
self.power = power

@abstractmethod
def attack(self, other, attack_kind):
pass

@abstractmethod
def get_damage(self, power, attack_kind):
pass

def __str__(self):
return '{} : {}'.format(self.name, self.HP)

3.2 Player 클래스 만들기

플레이어는 다음과 같은 특성이 있다.

  • 추가되는 멤버 : 플레이어는 다양한 공격 종류를 담을 수 있는 기술 목록이 있다.
  • attack : 플레이어는 공격할 때 공격 종류가 기술 목록 안에 있다면 상대 몬스터에게 피해를 입힌다.
  • get_damage : 플레이어가 피해를 입을 때 몬스터의 공격 종류가 플레이어의 기술 목록에 있다면 몬스터의 공격력이 반감되어 hp의 절반만 깎인다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Player(Character):
def __init__(self, name = 'player', hp = 100, power = 10, *attack_kinds):
super().__init__(name, hp, power)

self.skills = []

for attack_kind in attack_kinds:
self.skills.append(attack_kind)

def attack(self, other, attack_kind):
if attack_kind in self.skills:
other.get_damage(self.power, attack_kind)

def get_damage(self, power, attack_kind):
if attack_kind in self.skills:
self.HP -= (poewr/2)

else :
self.HP -= power

코드에서 살펴보면 플레이어는 캐릭터를 상속했다.

3.3 Monster, IceMonster, FireMonster 클래스 만들기

몬스터에는 불 몬스터와 얼음 몬스터가 있으며 다음과 같은 특징이 있다.

  • 추가되는 멤버 : 공격 종류를 가진다. 불 몬스터는 Fire, 얼음 몬스터는 Ice를 가진다.
  • 공통 메서드 : 두 몬스터는 같은 행동을 한다.
    • attack : 공격 종류가 몬스터의 속성과 같다면 공격한다.
    • get_damage : 몬스터는 자신과 속성이 같은 공격을 당하면 체력이 오히려 공격력만큼 증가한다. 그렇지 않으면 체력이 공격력만큼 감소한다.

여기서 고민해야 할 점이 있다. FireMonster 클래스와 IceMonster 클래스를 Character 클래스에서 상속받아 구현할지, Moster 클래스라는 부모 클래스를 따로 만들어야 할지.
설명에 따르면 추가되는 멤버도 겹치고, fireball()메서드를 제외한 나머지 메서드도 겹친다. 그러면 공통되는 부분을 기본 클래스로 만들고 이를 상속받는 게 좋을 것 같다.

몬스터를 만들고 몬스터는 캐릭터 클래스를 상속받을 것이다. 그리고 몬스터 클래스를 상속받아 각 몬스터를 만들어보자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Monster(Character):
def __init__(self, name, hp, power):
super().__init__(name, hp, power):
self.attack_kind = 'None'

def attack(self, other, attack_kind):
if self.attack_kind == attack_kind:
other.get_damage(self.power, attack_kind)

def get_damage(self, power, attack_kind):
if self.attack_kind == attack_kind:
self.HP += power
else :
self.HP -= power

def get_attack_kind(self):
return self.attack_kind


class IceMonster(Monster):
def __init__(self, name = 'Ice monster', hp = 50, power = 10)
super().__init__(name, hp, power)
self.attack_kind = 'ICE'


class FireMonster(Monster):
def __init__(self, name = 'Fire monster', hp= 50, power = 20)
super().__init__.(name, hp, power)
self.attack_kind = 'FIRE'

def firebreath(self):
print('firebreath')

불 몬스터와 얼음 몬스터는 몬스터 클래스를 상속 받았고 추가되거나 변경되는 부분만 수정했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
if __name__ == "__main__":
player = Player('sword master', 100, 30, 'ICE')
monsters = []
monsters.append(IceMonster())
monsters.append(FireMonster())

for monster in monsters :
print(monster)

for monster in monsters:
player.attack(monster, 'ICE')

print('after the plater attacked')

for monster in monsters:
print(monster)
print('')

print(player)

for monster in monsters:
monster.attack(player, monster.get_attack_kind())
print('after monsters attacked')

print(player)

4. 연산자 오버로딩

연산자 오버로딩은 클래스 안에서 메서드로 연산자를 새롭게 구현하는 것으로 다형성의 특별한 형태이다.
연산자 오버로딩을 사용하면 다른 객체나 일반적인 피연산자와 연산을 할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point:
def __init__(self, x=0, y=0):
self.x = x
self.y = y

def set_point(self, x, y):
self.x = x
self.y = y

def get_point(self):
return self.x, self,y

def __str__(self):
return '({x}, {y})'.format(x = self.x, y = self.y)

if __name__ == "__main__":
p1 = Point(2,2)
p2 = p1 + 3
print(p2)

결과를 실행해 보면 에러가 발생할 것이다. Point와 int 객체 사이는 덧셈을 할 수 없다고 나온다.

1
2
3
4
5
6
7
8
9
	def __add__(self, n):
x = self.x + n
y = self.y + n
return Point(x,y)

if __name__ == "__main__":
p1 = Point(2,2)
p2 = p1 + 3
print(p2)

이렇게 add메서드를 추가해보자. 예약한 함수를 사용해서 x좌표와 y좌표에 인자 n을 더한 새로운 x와 y로
새로운 객체를 만들어 반환한다.
실행 결과는 (5,5)가 나오게 된다.

1
2
3
4
if __name__ == "__main__":
p1 = Point(2,2)
p2 = 3 + p1
print(p2)

계산이 안된다. int와 Point의 순서가 바뀌면 에러가 발생한다.
연산자 오버로딩을 하나 더 해주자.

1
2
3
4
5
6
7
8
9
	def __radd__(self, n):
x = self.x + n
y = self.y + n
return Point(x,y)

if __name__ == "__main__":
p1 = Point(2,2)
p2 = 3 + p1
print(p2)
1
(5,5)

__radd__()메서드를 이용해서 연산이 돌아가도록 만들었다.

Python의 실수형에 대해서 알아보자

Python의 실수형에 대해서 알아보자

컴퓨터 사이언스 부트캠프 with Python

1. 실수 연산의 함정

데이터 사이언스 공부를 하다보면 가끔 머리로 이해되지 않는 것이 생기곤 한다. 그 중 하나가 실수 연산에 대한 것이다.
다음 예를 살펴보자

python(3.6.4)으로 다음과 같이 입력하자.

1
2
3
4
5
a = 0.01
result = 0.0
for i in range(100):
result += a
result

result는 값이 어떻게 나오게 될까?
위의 코드는 쉽게 말하자면 0.01을 100번 더한 것과 다를 게 없다.
그렇다면 답은 1일 것이다. 하지만

1
2
>>> result 
1.0000000000000007

답은 1이 아니다. 만약 내가 조건문을 이용해서
1==result 판단을 내렸다면 결과는 False로 나올 것이다.

1
2
3
4
a = 0.015625
for i in range(100):
result += a
result
1
>>>1.5625

그런데 이번 경우에는 생각과 같이 1.5625라는 결과가 나온다. 왜 갑자기 오차 하나 없이 깔끔하게 값이 나오는 것일까?
왜 이런 일이 발생하는 것일까?

2. 부동소수점

이 현상에 대해 이해하기 위해 부동소수점에 대해서 이해를 해야한다.

부동소수점에서 ‘부’는 부유한다는 말, 즉 떠다닌다는 말이다. 123.456을 다르게 표현해 보는 경우를 생각해보자

1
2
3
123.456 = 1.23456 * 10^2
123.456 = 12.3456 * 10
........

위의 예시 말고도 다양한 방식이 있다. 소숫점이 둥둥 떠다니는 것 같이 움직인다. 그래서 이러한 실수 표현 방식을 부동소수점이라고 부른다.

3. 단정도와 배정도

단정도(single-precision)는 실수를 32비트로 표현하며 부호 1비트, 지수부 8비트, 가수부 23비트로 구성되어 있다.
배정도(double-precision)는 실수를 64비트로 표현하며 부호 1비트, 지수부 11비트, 가수부 52비트로 구성되어 있다.
배정도가 단정도보다 두 배 정도의 비트 수가 많은데, 비트 수가 많은 만큼 정밀도가 높다고 할 수 있겠다. 파이썬은 배정도를 사용한다.

4. 1바이트 실수 자료형 설계하기

위의 수식은 실수 자료형을 표현한 수식이다.

1.man은 가수, 2는 밑수, exp-bias는 지수를 의미한다.

이 식을 이용해 7.75라는 10진수 실수를 1바이트 부동소수점으로 표현해보자.

4.1 10진수 실수를 2진수 실수로 바꾸기

2진수로 바꾸면 111.11이란 값이 나온다.

4.2 정규화

아 숫자를 정규화 해보자. 정규화란, 소수점 왼쪽에 위치한 가수 부분을 밑수보다 작은 자연수가 되도록 만드는 것이다.
111.11을 정규화 하면 다음과 같다.

4.3 메모리 구조

정규화된 부동소수점 $1.111 \times 2^2$를 앞의 수식과 비교해 보면
man은 1111이고 exp-bias는 2이다.
이제 메모리 구조를 정하고 man과 exp값만 저장하면 설계가 끝난다.
이때 지수부와 가수부에 할당하는 비트 수에 따라 표현 범위와 정밀도가 결정된다.

1바이트 부동소수점 구조는 다음과 같다.

  • 첫번째 비트 : 부호 0은 양수, 1은 음수
  • 가운데 4비트 : 지수부에 해당하며 exp 값이다.
    • 0~15의 양수를 표현할 수 있다
    • $bias = 2^{지수의 비트수} -1$
  • 맨 뒤 3비트 : 가수부로 man 값을 저장함

$1.1111 \times 2^2$를 1바이트의 메모리 구조로 변경해 보자.

  • 부호비트는 0이다.
  • $exp-bias$는 2이다. $bias$값이 7이므로 $exp$는 9가 된다.이것을 이진수로 나타내면 $1001_{(2)}$가 된다.
  • 가수부에 할당된 비트는 3비트이다. 1111을 3비트에 넣을때는 뒷자리 1을 생략한다. 가수부는 111이다.이것을 16진수로 나타내면 0x4f가 된다.
    정리하자면 10진수 7.75를 $ 0100 \ 1111 $로 나타낼 수 있고 이것을 다시 16진수로 나타내면 0x4f이다.

4.4 1바이트 부동소수점의 표현 범위

  • 표현할 수 있는 가장 작은 수(지수부0001)
    • $1.0000 \times = 0.0156256$
  • 표현할 수 있는 가장 큰 수(지수부 1110)
    • $1.111 \times = 240$

단, 지수부 비트가 모두 0일때와 모두 1일때는 0.0, 정규화 불가능, 무한대, NaN 같은 특별한 상황이므로 제외한다.

4.5 1바이트 부동소수점의 정밀도

7.75를 변환하는 과정에서 3비트의 가수부데이터에 1을 누락해 가면서 가수부 공간에 담았던 것을 기억할 것이다.
1을 누락하게 되면 0x4f는 7.75를 완벽하게 표현하지 못하게 된다.

여기서 0.25만큼 차이가 나게되고, 그만큼 정밀도도 떨어지게 된다.

5. 정밀도에 대한 고찰

5.1 엡실론

실수 자료형에서 엡실론이란 1.0과 그 다음으로 표현 가능한 수 사이의 차이를 말한다.

1
2
import sys
sys.float_info.epsilion

위 코드로 엡실론 값을 확인해 보자.
배정도의 가수부는 52비트인 것을 기억할 것이다. 1.0을 배정도에 맞춰 표현하면
$1.0000 ….. 0000(0:52개) \times 2^0$
배정도에서 1.0다음으로 표현할 수 있는 수는
$1.0000 ….. 0000(0:51개) \times 2^0$
두 수의 차이는
$1.0000 ….. 0000(0:51개) \times 2^0$
이 숫자를 10진수로 바꾸면 엡실론 값이 등장한다.
$2.220446049250313 \times 10^{-16}$

5.2 엡실론과 정밀도

엡실론을 이용하면 해당 실수 다음에 표현할 수 있는 수를 알아낼 수 있다.
9.25라는 수를 부동소수점 방식으로 표현하면 $1.00101 \times 2^3$이다.
여기서 지수부분만 떼서 엡실론을 구하면 이 실수와 다음 표현 가능한 수 사이의 차이를 구할 수 있다.
코드로 살펴보면,

1
2
3
4
5
6
7
8
9
import sys
ep = sys.float_info.epsilion
a= 9.25
diff = (2**3)*ep
diff
>>1.7763568394002504e-15
b = a + diff
b
>>>9.250000000000002

0.000000000000002만큼 차이가 난다.

그렇다면 9.25에 diff보다 작은 값을 더하면 어떻게 될까?
추측으로는 9.25가 나올 것 같다.
확인해보자

1
2
3
4
5
a = 9.25
half_diff = diff/2
c = a + half_diff
a == c
>>> True

추측과 같이 half_diff를 더하더라도 값의 변화가 없다.
diff보다 작은 값을 더한 수를 부동소수점 방식에서는 표현할 수 없다는 말이다.
다르게 말하자면 정밀도가 떨어진다는 말이다.

다음의 내용은 혼자서하는 괴발개발 블로그 https://aisolab.github.io/computer%20science/2018/08/07/CS_Real-number 에서 가져온 글이다.
다음의 방법을 이용하면 상대오차(relative error) 가 엡실론보다 작으면 서로 같은 수라고 판단하는 function을 만듦으로써 위와 같은 문제를 해결할 수 있다.

1
2
3
4
a = 0.1 * 3
b = 0.3

print(a == b)
1
False
1
2
3
4
5
6
7
8
9
import sys
def is_equal(a, b):
ep = sys.float_info.epsilon
return abs(a - b) <= max(abs(a), abs(b)) * ep

a = .1 * 3
b = .3

print(is_equal(a,b))
1
True