파이썬 코드 최적화: 메모리 사용 줄이기 🐍💾
안녕하세요, 파이썬 개발자 여러분! 오늘은 아주 흥미진진한 주제로 여러분과 함께 시간을 보내려고 해요. 바로 파이썬 코드 최적화, 그 중에서도 메모리 사용을 줄이는 방법에 대해 깊이 있게 알아볼 거예요. 🚀
여러분, 혹시 자신의 파이썬 프로그램이 너무 많은 메모리를 사용해서 고민해본 적 있나요? 아니면 대용량 데이터를 처리하는 과정에서 "메모리 에러"와 씨름한 경험이 있으신가요? 그렇다면 이 글이 여러분에게 꼭 필요한 해답이 될 거예요!
우리는 마치 재능넷(https://www.jaenung.net)에서 고급 파이썬 강의를 듣는 것처럼, 차근차근 그리고 재미있게 이 주제에 대해 탐구해 볼 거예요. 자, 그럼 우리의 파이썬 코드 다이어트 여정을 시작해볼까요? 🏋️♂️💻
1. 파이썬과 메모리: 기본 이해하기 🧠
먼저, 파이썬이 메모리를 어떻게 관리하는지 이해하는 것부터 시작해볼까요? 이것은 마치 우리가 집을 정리하기 전에 집 구조를 이해하는 것과 같아요. 😊
1.1 파이썬의 메모리 관리 특징
- 동적 타이핑: 파이썬은 변수의 타입을 실행 시간에 결정해요. 이는 편리하지만, 때로는 예상치 못한 메모리 사용으로 이어질 수 있어요.
- 가비지 컬렉션: 파이썬은 자동으로 메모리를 관리해요. 더 이상 사용되지 않는 객체를 찾아 메모리를 해제하죠.
- 레퍼런스 카운팅: 객체가 참조될 때마다 카운트가 증가하고, 참조가 해제될 때 감소해요. 카운트가 0이 되면 객체는 메모리에서 제거돼요.
💡 재능넷 팁: 파이썬의 메모리 관리 방식을 이해하면, 효율적인 코드 작성에 큰 도움이 됩니다. 재능넷에서 제공하는 파이썬 고급 과정에서 이런 내용을 더 자세히 배울 수 있어요!
1.2 파이썬 객체의 메모리 구조
파이썬의 모든 것은 객체예요. 정수, 문자열, 함수, 클래스 등 모든 것이 객체로 취급되죠. 각 객체는 다음과 같은 정보를 포함하고 있어요:
- 타입 정보
- 레퍼런스 카운트
- 값
이런 구조 때문에 파이썬 객체는 다른 저수준 언어의 변수보다 더 많은 메모리를 사용할 수 있어요. 하지만 걱정 마세요! 이 글에서 우리는 이런 특성을 고려하여 메모리를 최적화하는 방법을 배울 거예요. 😉
이 그림을 보면 파이썬 객체가 어떻게 구성되어 있는지 한눈에 알 수 있죠? 이제 우리는 파이썬 객체의 기본 구조를 이해했으니, 다음 단계로 넘어갈 준비가 되었어요!
2. 메모리 사용량 측정하기 📏
자, 이제 우리의 파이썬 프로그램이 얼마나 많은 메모리를 사용하고 있는지 알아볼 차례예요. 이는 마치 다이어트를 시작하기 전에 체중을 재는 것과 같아요. 시작점을 알아야 얼마나 개선되었는지 알 수 있겠죠? 🤓
2.1 sys.getsizeof() 사용하기
sys.getsizeof()
함수는 파이썬 객체의 메모리 사용량을 바이트 단위로 알려줘요. 간단하게 사용할 수 있는 방법이죠.
import sys
# 정수형
num = 42
print(f"정수 {num}의 크기: {sys.getsizeof(num)} 바이트")
# 문자열
text = "Hello, World!"
print(f"문자열 '{text}'의 크기: {sys.getsizeof(text)} 바이트")
# 리스트
my_list = [1, 2, 3, 4, 5]
print(f"리스트 {my_list}의 크기: {sys.getsizeof(my_list)} 바이트")
이 코드를 실행하면 각 객체가 얼마나 많은 메모리를 차지하고 있는지 알 수 있어요. 하지만 주의할 점이 있어요!
⚠️ 주의: sys.getsizeof()
는 객체 자체의 크기만 측정해요. 객체가 참조하는 다른 객체의 크기는 포함하지 않죠. 예를 들어, 리스트의 경우 리스트 객체 자체의 크기만 측정하고 리스트 내부의 요소들의 크기는 측정하지 않아요.
2.2 memory_profiler 사용하기
좀 더 상세한 메모리 사용량을 알고 싶다면 memory_profiler
라이브러리를 사용할 수 있어요. 이 도구는 코드의 각 라인별로 메모리 사용량을 보여주기 때문에 매우 유용해요.
먼저, memory_profiler
를 설치해야 해요:
pip install memory_profiler
그리고 다음과 같이 사용할 수 있어요:
@profile
def memory_hungry_function():
big_list = [i for i in range(1000000)]
del big_list
return "Done"
if __name__ == '__main__':
memory_hungry_function()
이 코드를 example.py
라는 이름으로 저장한 후, 다음 명령어로 실행해보세요:
python -m memory_profiler example.py
그러면 각 라인별로 메모리 사용량이 표시될 거예요. 이를 통해 어느 부분에서 메모리 사용량이 급증하는지 쉽게 파악할 수 있죠.
이 그래프를 보면, 두 번째 줄에서 메모리 사용량이 급격히 증가하고, 세 번째 줄에서 다시 감소하는 것을 볼 수 있어요. 이렇게 시각적으로 메모리 사용량을 확인하면 어느 부분을 최적화해야 할지 쉽게 알 수 있죠.
💡 재능넷 팁: 메모리 프로파일링은 대규모 데이터 처리나 복잡한 알고리즘을 다룰 때 특히 유용해요. 재능넷에서 제공하는 '파이썬 성능 최적화' 강좌에서 이런 고급 기술을 더 자세히 배울 수 있답니다!
자, 이제 우리는 파이썬 프로그램의 메모리 사용량을 측정하는 방법을 배웠어요. 이 지식을 바탕으로 다음 섹션에서는 실제로 메모리 사용량을 줄이는 다양한 테크닉을 알아볼 거예요. 준비되셨나요? Let's go! 🚀
3. 데이터 구조 최적화하기 🏗️
이제 본격적으로 메모리 사용량을 줄이는 방법을 알아볼 차례예요. 첫 번째로 살펴볼 것은 데이터 구조를 최적화하는 방법이에요. 적절한 데이터 구조를 선택하는 것만으로도 메모리 사용량을 크게 줄일 수 있답니다! 😃
3.1 리스트 대신 제너레이터 사용하기
리스트는 파이썬에서 가장 많이 사용되는 데이터 구조 중 하나예요. 하지만 큰 데이터셋을 다룰 때는 메모리를 많이 사용할 수 있어요. 이럴 때 제너레이터를 사용하면 메모리 사용량을 크게 줄일 수 있답니다.
# 메모리를 많이 사용하는 리스트 컴프리헨션
big_list = [i * i for i in range(1000000)]
# 메모리 효율적인 제너레이터 표현식
big_gen = (i * i for i in range(1000000))
제너레이터는 모든 결과를 한 번에 메모리에 저장하지 않고, 필요할 때마다 하나씩 생성해요. 이는 메모리 사용량을 크게 줄일 수 있는 아주 효과적인 방법이죠.
🌟 Pro Tip: 대용량 파일을 처리할 때도 제너레이터를 활용할 수 있어요. for line in open('big_file.txt')
와 같이 사용하면, 파일의 내용을 한 번에 메모리에 올리지 않고 한 줄씩 처리할 수 있답니다.
3.2 집합(Set) 활용하기
중복을 허용하지 않는 데이터를 다룰 때는 리스트 대신 집합(Set)을 사용하는 것이 좋아요. 집합은 해시 테이블을 기반으로 하기 때문에 검색과 삽입이 매우 빠르고, 중복 요소를 자동으로 제거해줘요.
# 리스트 사용 (중복 허용)
fruit_list = ['apple', 'banana', 'apple', 'cherry', 'banana', 'date']
# 집합 사용 (중복 제거)
fruit_set = set(['apple', 'banana', 'apple', 'cherry', 'banana', 'date'])
print(f"리스트 길이: {len(fruit_list)}") # 출력: 6
print(f"집합 길이: {len(fruit_set)}") # 출력: 4
이 예제에서 볼 수 있듯이, 집합을 사용하면 중복된 요소를 자동으로 제거할 수 있어요. 이는 데이터의 고유성을 유지하면서도 메모리 사용량을 줄일 수 있는 좋은 방법이에요.
3.3 튜플 사용하기
변경할 필요가 없는 데이터라면 리스트 대신 튜플을 사용하는 것이 좋아요. 튜플은 불변(immutable)이기 때문에 리스트보다 메모리를 덜 사용하고, 약간 더 빠른 성능을 제공해요.
# 리스트 사용
coordinates_list = [1, 2, 3]
# 튜플 사용
coordinates_tuple = (1, 2, 3)
print(f"리스트 크기: {sys.getsizeof(coordinates_list)} 바이트")
print(f"튜플 크기: {sys.getsizeof(coordinates_tuple)} 바이트")
이 코드를 실행해보면, 튜플이 리스트보다 조금 더 적은 메모리를 사용하는 것을 확인할 수 있어요. 작은 차이지만, 대량의 데이터를 다룰 때는 이 차이가 꽤 커질 수 있답니다!
3.4 collections 모듈 활용하기
파이썬의 collections
모듈은 메모리 효율적인 여러 데이터 구조를 제공해요. 그 중 몇 가지를 살펴볼까요?
- namedtuple: 이름 있는 필드를 가진 튜플 서브클래스를 만들 수 있어요. 일반 클래스보다 메모리를 덜 사용해요.
- deque: 양쪽 끝에서 빠르게 추가/삭제할 수 있는 리스트 형 컨테이너예요. 큰 리스트의 양 끝에서 자주 연산을 수행할 때 유용해요.
- Counter: 요소의 개수를 세는 데 특화된 딕셔너리 서브클래스예요.
from collections import namedtuple, deque, Counter
# namedtuple 사용
Point = namedtuple('Point', ['x', 'y'])
p = Point(1, 2)
print(f"x: {p.x}, y: {p.y}")
# deque 사용
d = deque([1, 2, 3, 4, 5])
d.appendleft(0)
d.append(6)
print(d)
# Counter 사용
c = Counter('abracadabra')
print(c)
이러한 특수 데이터 구조들은 각각의 용도에 맞게 최적화되어 있어, 적절히 사용하면 메모리 사용량과 성능을 모두 개선할 수 있어요.
이 그래프를 보면, 같은 데이터를 다룰 때도 어떤 데이터 구조를 선택하느냐에 따라 메모리 사용량이 크게 달라질 수 있다는 것을 알 수 있어요. 제너레이터가 가장 메모리 효율적이고, 그 다음으로 집합, 튜플, 리스트 순이에요.
💡 재능넷 팁: 데이터 구조의 선택은 프로그램의 성능에 큰 영향을 미칩니다. 재능넷에서 제공하는 '파이썬 고급 데이터 구조' 강좌를 통해 각 데이터 구조의 특성과 적절한 사용 시기를 더 자세히 배워보세요!
자, 이제 우리는 파이썬의 다양한 데이터 구조와 그 특성에 대해 알아봤어요. 각 상황에 맞는 적절한 데이터 구조를 선택하는 것만으로도 메모리 사용량을 크게 줄일 수 있다는 걸 기억하세요. 다음 섹션에서는 더 세부적인 코드 최적화 기법에 대해 알아볼 거예요. 계속해서 흥미진진한 파이썬 최적화 여정을 이어가볼까요? 🚀
4. 메모리 누수 방지하기 🚰
메모리 누수는 프로그램이 더 이상 필요하지 않은 메모리를 계속 점유하고 있는 현상을 말해요. 파이썬은 가비지 컬렉션을 통해 자동으로 메모리를 관리하지만, 때로는 개발자의 실수로 메모리 누수가 발생할 수 있어요. 이번 섹션에서는 메모리 누수를 방지하는 방법에 대해 알아볼거예요. 🕵️♂️
4.1 순환 참조 피하기
순환 참조는 두 개 이상의 객체가 서로를 참조하는 상황을 말해요. 이런 경우, 가비지 컬렉터가 해당 객체들 을 메모리에서 해제하지 못할 수 있어요. 다음은 순환 참조의 예시입니다:
class Node:
def __init__(self):
self.ref = None
node1 = Node()
node2 = Node()
node1.ref = node2
node2.ref = node1
# 이제 node1과 node2는 서로를 참조하고 있어요
이런 상황을 피하기 위해서는 약한 참조(weak reference)를 사용할 수 있어요:
import weakref
class Node:
def __init__(self):
self.ref = None
node1 = Node()
node2 = Node()
node1.ref = node2
node2.ref = weakref.ref(node1)
# 이제 node2는 node1을 약하게 참조하고 있어요
약한 참조를 사용하면 가비지 컬렉터가 순환 참조를 감지하고 메모리를 해제할 수 있어요.
💡 재능넷 팁: 복잡한 객체 관계를 다룰 때는 항상 순환 참조의 가능성을 고려해야 해요. 재능넷의 '고급 파이썬 객체 지향 프로그래밍' 강좌에서 이런 문제를 효과적으로 다루는 방법을 더 자세히 배울 수 있어요!
4.2 대용량 객체 사용 후 명시적으로 해제하기
큰 객체를 사용한 후에는 명시적으로 메모리를 해제하는 것이 좋아요. 파이썬의 del
키워드를 사용하면 돼요:
# 대용량 데이터 생성
big_data = [i for i in range(1000000)]
# 데이터 처리
process_data(big_data)
# 데이터 사용이 끝났으면 명시적으로 해제
del big_data
del
을 사용하면 객체에 대한 참조를 즉시 제거할 수 있어요. 이렇게 하면 가비지 컬렉터가 더 빨리 메모리를 회수할 수 있답니다.
4.3 제너레이터 활용하기
앞서 언급했듯이, 제너레이터를 사용하면 대용량 데이터를 처리할 때 메모리 사용을 크게 줄일 수 있어요. 다음은 파일을 읽을 때 제너레이터를 활용하는 예시에요:
def read_large_file(file_path):
with open(file_path, 'r') as file:
for line in file:
yield line.strip()
# 사용 예
for line in read_large_file('very_large_file.txt'):
process_line(line)
이 방법을 사용하면 파일의 내용을 한 번에 메모리에 올리지 않고, 한 줄씩 처리할 수 있어요. 대용량 파일을 다룰 때 매우 유용한 방법이죠!
4.4 메모리 프로파일링 도구 사용하기
메모리 누수를 찾아내는 가장 좋은 방법 중 하나는 메모리 프로파일링 도구를 사용하는 거예요. 앞서 소개한 memory_profiler
외에도 tracemalloc
이라는 파이썬 내장 모듈을 사용할 수 있어요:
import tracemalloc
tracemalloc.start()
# 여기에 메모리 사용량을 측정하고 싶은 코드를 넣으세요
big_list = [i for i in range(100000)]
snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')
for stat in top_stats[:10]:
print(stat)
이 코드는 메모리 사용량이 가장 많은 상위 10개의 라인을 보여줘요. 이를 통해 어느 부분에서 메모리를 많이 사용하는지 쉽게 파악할 수 있답니다.
이 그림은 우리가 지금까지 배운 메모리 누수 방지 전략을 요약하고 있어요. 각각의 전략을 적절히 활용하면 메모리 누수를 효과적으로 예방할 수 있답니다.
🌟 Pro Tip: 메모리 누수 문제는 종종 프로그램이 오랜 시간 동안 실행될 때 발생해요. 따라서 장기 실행 테스트를 통해 메모리 사용량의 변화를 관찰하는 것이 중요합니다. 재능넷의 '파이썬 성능 최적화 및 디버깅' 강좌에서 이런 고급 테스트 기법을 배울 수 있어요!
자, 이제 우리는 메모리 누수를 방지하는 여러 가지 방법에 대해 알아봤어요. 이런 기법들을 적절히 활용하면 프로그램의 메모리 효율성을 크게 높일 수 있답니다. 다음 섹션에서는 더 세부적인 코드 최적화 기법에 대해 알아볼 거예요. 계속해서 흥미진진한 파이썬 최적화 여정을 이어가볼까요? 🚀
5. 코드 최적화 기법 🛠️
지금까지 우리는 데이터 구조와 메모리 관리에 대해 알아봤어요. 이제는 코드 자체를 최적화하는 방법에 대해 알아볼 차례입니다. 작은 변화로도 큰 성능 향상을 얻을 수 있다는 걸 기억하세요! 😉
5.1 리스트 컴프리헨션 활용하기
리스트 컴프리헨션은 간결하고 읽기 쉬울 뿐만 아니라, 일반적인 for 루프보다 더 빠르게 동작해요.
# 일반적인 for 루프
squares = []
for i in range(10):
squares.append(i**2)
# 리스트 컴프리헨션
squares = [i**2 for i in range(10)]
리스트 컴프리헨션은 내부적으로 최적화되어 있어, 같은 작업을 하는 for 루프보다 더 빠르게 동작합니다.
5.2 내장 함수 활용하기
파이썬의 내장 함수들은 C로 구현되어 있어 매우 빠르게 동작해요. 가능하다면 직접 구현하는 것보다 내장 함수를 사용하는 것이 좋습니다.
# 직접 구현 (느림)
def my_sum(numbers):
total = 0
for num in numbers:
total += num
return total
# 내장 함수 사용 (빠름)
total = sum(numbers)
5.3 지역 변수 활용하기
전역 변수보다는 지역 변수를 사용하는 것이 더 빠릅니다. 파이썬은 지역 변수에 더 빠르게 접근할 수 있기 때문이에요.
# 전역 변수 사용 (느림)
global_var = 10
def slow_function():
for i in range(1000000):
global_var += 1
# 지역 변수 사용 (빠름)
def fast_function():
local_var = 10
for i in range(1000000):
local_var += 1
5.4 문자열 연결 최적화하기
문자열을 연결할 때는 +
연산자보다 join()
메서드를 사용하는 것이 더 효율적이에요.
# 비효율적인 방법
result = ''
for i in range(1000):
result += str(i)
# 효율적인 방법
result = ''.join(str(i) for i in range(1000))
join()
메서드는 내부적으로 최적화되어 있어, 많은 문자열을 연결할 때 특히 효과적입니다.
5.5 적절한 자료형 선택하기
작업의 특성에 맞는 자료형을 선택하는 것이 중요해요. 예를 들어, 집합 연산을 자주 하는 경우에는 리스트보다 set을 사용하는 것이 좋습니다.
# 리스트 사용 (느림)
list1 = [1, 2, 3, 4, 5]
list2 = [4, 5, 6, 7, 8]
common = [x for x in list1 if x in list2]
# 집합 사용 (빠름)
set1 = set([1, 2, 3, 4, 5])
set2 = set([4, 5, 6, 7, 8])
common = set1.intersection(set2)
이 그래프는 각각의 최적화 기법이 성능에 미치는 영향을 시각적으로 보여줍니다. 최적화된 코드와 자료형을 사용할수록 성능이 향상되는 것을 볼 수 있어요.
💡 재능넷 팁: 코드 최적화는 항상 측정 가능한 방식으로 이루어져야 해요. 재능넷의 '파이썬 성능 최적화' 강좌에서는 각종 프로파일링 도구를 사용해 코드의 성능을 정확히 측정하고 개선하는 방법을 배울 수 있답니다!
자, 이제 우리는 파이썬 코드를 최적화하는 여러 가지 방법에 대해 알아봤어요. 이런 기법들을 적절히 활용하면 프로그램의 성능을 크게 향상시킬 수 있답니다. 하지만 기억하세요, 최적화는 항상 가독성과 유지보수성을 고려하면서 이루어져야 해요. 다음 섹션에서는 이러한 최적화 기법들을 실제 프로젝트에 적용하는 방법에 대해 알아볼 거예요. 준비되셨나요? Let's optimize! 🚀
6. 실제 프로젝트에 적용하기 🏗️
지금까지 우리는 다양한 메모리 최적화 기법과 코드 최적화 방법에 대해 알아봤어요. 이제 이 모든 것을 실제 프로젝트에 어떻게 적용할 수 있는지 살펴볼 차례입니다. 실제 상황에서 이러한 기법들을 적용하면 어떤 결과가 나올지, 함께 알아볼까요? 🕵️♂️
6.1 대용량 파일 처리 최적화
대용량 파일을 처리하는 경우, 메모리 사용량을 최소화하면서 효율적으로 처리하는 것이 중요해요. 다음은 대용량 CSV 파일을 처리하는 예시입니다:
import csv
from collections import defaultdict
def process_large_csv(file_path):
word_count = defaultdict(int)
with open(file_path, 'r') as file:
reader = csv.reader(file)
for row in reader:
for word in row:
word_count[word.lower()] += 1
return word_count
# 사용 예
result = process_large_csv('very_large_file.csv')
print(result)
이 코드는 다음과 같은 최적화 기법을 사용하고 있어요:
- 파일을 한 번에 메모리에 로드하지 않고, 한 줄씩 처리합니다.
defaultdict
를 사용해 딕셔너리 접근을 최적화했어요.- 제너레이터를 활용해 메모리 사용을 최소화했습니다.
6.2 웹 스크래핑 최적화
웹 스크래핑을 할 때는 비동기 처리를 통해 성능을 크게 향상시킬 수 있어요. 다음은 aiohttp
와 asyncio
를 사용한 비동기 웹 스크래핑 예시입니다:
import asyncio
import aiohttp
from bs4 import BeautifulSoup
async def fetch(session, url):
async with session.get(url) as response:
return await response.text()
async def scrape(urls):
async with aiohttp.ClientSession() as session:
tasks = [fetch(session, url) for url in urls]
responses = await asyncio.gather(*tasks)
return [BeautifulSoup(response, 'html.parser') for response in responses]
# 사용 예
urls = ['http://example.com', 'http://example.org', 'http://example.net']
results = asyncio.run(scrape(urls))
이 코드는 다음과 같은 최적화 기법을 사용하고 있어요:
- 비동기 처리를 통해 I/O 작업을 병렬로 처리합니다.
aiohttp
를 사용해 네트워크 요청을 최적화했어요.asyncio.gather()
를 사용해 여러 작업을 동시에 처리합니다.
6.3 데이터 분석 최적화
대용량 데이터를 분석할 때는 pandas와 numpy를 효율적으로 사용하는 것이 중요해요. 다음은 대용량 데이터프레임을 처리하는 예시입니다:
import pandas as pd
import numpy as np
def optimize_dataframe(df):
# 메모리 사용량 줄이기
for col in df.columns:
if df[col].dtype == 'object':
df[col] = pd.Categorical(df[col])
elif df[col].dtype == 'float64':
df[col] = df[col].astype('float32')
# 계산 최적화
df['new_column'] = np.where(df['condition_column'] > 0,
df['value_column1'],
df['value_column2'])
return df
# 사용 예
large_df = pd.read_csv('large_data.csv')
optimized_df = optimize_dataframe(large_df)
이 코드는 다음과 같은 최적화 기법을 사용하고 있어요:
- 카테고리형 데이터를 사용해 메모리 사용량을 줄였어요.
- float64 대신 float32를 사용해 메모리를 절약했습니다.
np.where()
를 사용해 벡터화된 연산을 수행했어요.
이 그래프는 최적화 전후의 성능 차이를 보여줍니다. 최적화를 통해 실행 시간과 메모리 사용량을 크게 줄일 수 있다는 것을 알 수 있어요.
💡 재능넷 팁: 실제 프로젝트에서는 여러 최적화 기법을 조합해서 사용하는 것이 중요해요. 재능넷의 '실전 파이썬 프로젝트' 강좌에서는 다양한 실제 시나리오에서 이러한 최적화 기법들을 어떻게 적용하는지 배울 수 있답니다!
자, 이제 우리는 실제 프로젝트에서 파이썬 코드를 어떻게 최적화할 수 있는지 알아봤어요. 이러한 기법들을 적절히 활용하면 프로그램의 성능을 크게 향상시킬 수 있답니다. 하지만 기억하세요, 최적화는 항상 측정 가능한 방식으로 이루어져야 해요. 성능 향상과 코드의 가독성, 유지보수성 사이의 균형을 잘 맞추는 것이 중요합니다. 이제 여러분은 파이썬 코드 최적화의 달인이 되었어요! 🎉 이 지식을 활용해 여러분의 프로젝트를 한 단계 더 발전시켜 보세요. 코딩의 세계는 끝이 없답니다. 계속해서 학습하고, 실험하고, 성장해 나가세요. 여러분의 코딩 여정에 행운이 함께하기를 바랍니다! 🚀🐍