Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Materials for the Python Copy [UPDATE] #657

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions python-copy/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Shallow vs Deep Copying of Python Objects

This folder contains code associated with the Real Python tutorial [Shallow vs Deep Copying of Python Objects](https://realpython.com/python-copy/).
59 changes: 59 additions & 0 deletions python-copy/benchmark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from copy import copy
from random import choices, randint
from string import ascii_letters
from timeit import timeit

EXECUTIONS = 10_000
SIZE = 1_000


def main():
print(f" Size={SIZE:,} ({EXECUTIONS:,}x) ".center(50, "="))
for container_type in random_dict, random_set, random_list:
container = container_type(size=SIZE)
for method, seconds in benchmark(container, EXECUTIONS).items():
print(f"{seconds:.5f} {method}")
print()


def benchmark(container, executions):
type_ = type(container)
name = type_.__name__
results = {
f"{name}.copy()": timeit(container.copy, number=executions),
f"{name}()": timeit(lambda: type_(container), number=executions),
f"copy({name})": timeit(lambda: copy(container), number=executions),
}
if sliceable(container):
results[f"{name}[:]"] = timeit(lambda: container[:], number=executions)
return results


def sliceable(instance):
try:
instance[0:1]
except (TypeError, KeyError):
return False
else:
return True


def random_dict(size):
keys = random_set(size)
values = random_set(size)
return dict(zip(keys, values))


def random_set(size):
return set(random_list(size))


def random_list(size, shortest=3, longest=15):
return [
"".join(choices(ascii_letters, k=randint(shortest, longest)))
for _ in range(size)
]


if __name__ == "__main__":
main()
42 changes: 42 additions & 0 deletions python-copy/console.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import copy
from datetime import UTC, datetime
from pprint import pp


class ConsoleWindow:
def __init__(self, tabs):
self.tabs = tabs
self.history = []
self.created_at = datetime.now(UTC)

def __copy__(self):
instance = type(self)(self.tabs)
instance.history = self.history
self.tabs.add(instance)
return instance

def __deepcopy__(self, memo):
instance = type(self)(self.tabs)
instance.history = copy.deepcopy(self.history, memo)
self.tabs.add(instance)
return instance

def run_command(self, command):
self.history.append(command)


if __name__ == "__main__":
shared_registry = set()
window = ConsoleWindow(shared_registry)
window.run_command("cd ~/Projects")
tab1 = copy.deepcopy(window)
tab1.run_command("git clone git@github.com:python/cpython.git")
tab2 = copy.copy(tab1)
tab2.run_command("cd python/")
window.run_command("ls -l")
tab1.run_command("git checkout 3.13")
pp(vars(window))
print()
pp(vars(tab1))
print()
pp(vars(tab2))
37 changes: 37 additions & 0 deletions python-copy/datafile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import copy
import json
from pprint import pp


class DataFile:
def __init__(self, path):
self.file = open(path, mode="r", encoding="utf-8")

def __copy__(self):
return type(self)(self.file.name)

def __deepcopy__(self, memo):
return self.__copy__()

def __enter__(self):
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.file.close()

def read_json(self):
self.file.seek(0)
return json.load(self.file)


if __name__ == "__main__":
with DataFile("person.json") as data_file:
shallow_copy = copy.copy(data_file)
deep_copy = copy.deepcopy(data_file)
pp(data_file.read_json())
print()
pp(shallow_copy.read_json())
shallow_copy.file.close()
print()
pp(deep_copy.read_json())
deep_copy.file.close()
24 changes: 24 additions & 0 deletions python-copy/emoji.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import unicodedata


class Emoji:
def __init__(self, name):
self.name = name

def __repr__(self):
return self._glyph

@property
def name(self):
return unicodedata.name(self._glyph).title()

@name.setter
def name(self, value):
self._glyph = unicodedata.lookup(value)


if __name__ == "__main__":
emoji = Emoji("tangerine")
print(emoji)
emoji.name = "clown face"
print(emoji)
24 changes: 24 additions & 0 deletions python-copy/golden_ratio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from fractions import Fraction

cf = [1]
cf.append(cf)


def evaluate(depth):
if depth > 0:
return cf[0] + Fraction(1, evaluate(depth - 1))
return cf[0]


golden_ratio = (1 + 5**0.5) / 2
for n in range(21):
fraction = evaluate(n)
approximation = float(fraction)
error = abs(golden_ratio - approximation)
print(
f"n={n:<3}",
f"{fraction:>11}",
"\N{ALMOST EQUAL TO}",
f"{approximation:.15f}",
f"(\N{GREEK CAPITAL LETTER DELTA} = {error:<11.10f})",
)
16 changes: 16 additions & 0 deletions python-copy/mutable_int.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
class MutableInt:
def __init__(self, value):
self.value = value

def __iadd__(self, other):
self.value += other
return self

def __str__(self):
return str(self.value)


if __name__ == "__main__":
x = MutableInt(40)
x += 2
print(x)
13 changes: 13 additions & 0 deletions python-copy/person.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "John Doe",
"age": 42,
"email": "johndoe@example.com",
"is_subscribed": true,
"hobbies": ["reading", "cycling", "traveling"],
"address": {
"street": "123 Main St",
"city": "New York",
"zip": "10001"
}
}

107 changes: 107 additions & 0 deletions python-copy/person.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import copy
from datetime import date


def in_place():
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __replace__(self, **changes):
if unknown := changes.keys() - self.__dict__.keys():
raise AttributeError(", ".join(unknown))
self.__dict__.update(**changes)

person = Person("John Doe", 42)
copy.replace(person, age=24, name="Alice Smith")
print(vars(person))
# This raises an error:
# print(copy.replace(person, name="Bob Brown", email="bob.brown@example.com"))


def shallow():
class Person:
def __init__(self, name, age):
self.name = name
self.age = age

def __replace__(self, **changes):
return type(self)(**self.__dict__ | changes)

person = Person("John Doe", 42)
person_copy = copy.replace(person, age=24, name="Alice Smith")
print(vars(person))
print(vars(person_copy))
# This raises an error:
# print(copy.replace(person, email="bob.brown@example.com"))


def slots():
class Person:
__slots__ = ("name", "age")

def __init__(self, name, age):
self.name = name
self.age = age

def __replace__(self, **changes):
instance = type(self)(self.name, self.age)
for name, value in changes.items():
if hasattr(self, name):
setattr(instance, name, value)
return instance

person = Person("John Doe", 42)
person_copy = copy.replace(person, age=24, name="Alice Smith")

def vars_slots(obj):
return {name: getattr(obj, name) for name in obj.__slots__}

print(vars_slots(person))
print(vars_slots(person_copy))


def derived():
class Person:
def __init__(self, name, date_of_birth):
self.name = name
self.date_of_birth = date_of_birth

@property
def age(self):
return (date.today() - self.date_of_birth).days // 365

def __replace__(self, **changes):
age = changes.pop("age", None)
dob = changes.pop("date_of_birth", None)

instance = copy.copy(self)
for name, value in changes.items():
if hasattr(self, name):
setattr(instance, name, value)

if age and dob:
raise AttributeError(
"can't set both 'age' and 'date_of_birth'"
)
elif age:
dob = copy.replace(date.today(), year=date.today().year - age)
instance.date_of_birth = dob
elif dob:
instance.date_of_birth = dob

return instance

person = Person("John Doe", date(1983, 3, 14))
print(vars(copy.replace(person, age=24)))
print(vars(copy.replace(person, date_of_birth=date(1999, 6, 15))))
# This raises an error:
# print(vars(copy.replace(person, date_of_birth=date(1999, 6, 15), age=12)))


if __name__ == "__main__":
in_place()
shallow()
slots()
derived()
34 changes: 34 additions & 0 deletions python-copy/rectangle.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import copy


class Rectangle:
def __init__(self, top_left, bottom_right):
self.top_left = top_left
self.bottom_right = bottom_right

def __repr__(self):
return f"Rectangle({self.top_left}, {self.bottom_right})"


class Point:
def __init__(self, x, y):
self.x = x
self.y = y

def __repr__(self):
return f"Point(x={self.x}, y={self.y})"


if __name__ == "__main__":
bounding_box = Rectangle(
top_left := Point(10, 20), bottom_right := Point(30, 40)
)
shallow_copy = copy.copy(bounding_box)
deep_copy = copy.deepcopy(bounding_box)

bounding_box.bottom_right = Point(500, 700)
bottom_right.x += 100

print(f"{bounding_box = }")
print(f"{shallow_copy = }")
print(f"{deep_copy = }")